كيفية إنشاء موقع للتجارة الإلكترونية باستخدام Angular 11 وطبقة التجارة و Paypal

نشرت: 2022-03-10
ملخص سريع ↬ يعد امتلاك متجر للتجارة الإلكترونية أمرًا بالغ الأهمية لأي صاحب متجر حيث يتجه المزيد والمزيد من العملاء إلى التسوق عبر الإنترنت. في هذا البرنامج التعليمي ، سنتعرف على كيفية إنشاء موقع للتجارة الإلكترونية باستخدام Angular 11. سيستخدم الموقع Commerce Layer كواجهة برمجة تطبيقات للتجارة الإلكترونية بدون رأس ويستخدم Paypal لمعالجة المدفوعات.

في الوقت الحاضر ، من الضروري أن يكون لديك وجود عبر الإنترنت عند إدارة الأعمال التجارية. يتم التسوق عبر الإنترنت أكثر بكثير مما كان عليه في السنوات السابقة. يسمح امتلاك متجر للتجارة الإلكترونية لأصحاب المتاجر بفتح مصادر أخرى للإيرادات لا يمكنهم الاستفادة منها بمجرد وجود متجر من الطوب وقذائف الهاون. ومع ذلك ، فإن أصحاب المتاجر الآخرين يديرون أعمالهم عبر الإنترنت بالكامل دون وجود مادي. هذا يجعل وجود متجر على الإنترنت أمرًا بالغ الأهمية.

تجعل مواقع مثل Etsy و Shopify و Amazon من السهل إنشاء متجر بسرعة كبيرة دون الحاجة إلى القلق بشأن تطوير موقع. ومع ذلك ، قد تكون هناك حالات قد يرغب فيها أصحاب المتاجر في الحصول على تجربة شخصية أو ربما توفير تكلفة امتلاك متجر على بعض هذه الأنظمة الأساسية.

توفر منصات واجهة برمجة التطبيقات للتجارة الإلكترونية بدون رؤوس خلفيات خلفية يمكن لمواقع المتاجر التفاعل معها. يديرون جميع العمليات والبيانات المتعلقة بالمتجر مثل العملاء والطلبات والشحنات والمدفوعات وما إلى ذلك. كل ما هو مطلوب هو واجهة أمامية للتفاعل مع هذه المعلومات. يمنح هذا المالكين قدرًا كبيرًا من المرونة عندما يتعلق الأمر بتحديد كيفية تجربة عملائهم لمتجرهم على الإنترنت وكيف يختارون تشغيله.

في هذه المقالة ، سنغطي كيفية إنشاء متجر للتجارة الإلكترونية باستخدام Angular 11. وسوف نستخدم Commerce Layer كواجهة برمجة تطبيقات للتجارة الإلكترونية بدون رأس. على الرغم من أنه قد يكون هناك الكثير من الطرق لمعالجة المدفوعات ، فسوف نوضح كيفية استخدام طريقة واحدة فقط ، وهي Paypal.

  • عرض التعليمات البرمجية المصدر على جيثب →

المتطلبات الأساسية

قبل إنشاء التطبيق ، تحتاج إلى تثبيت Angular CLI. سنستخدمه لتهيئة التطبيق وسقالاته. إذا لم يكن مثبتًا لديك بعد ، فيمكنك الحصول عليه من خلال npm.

 npm install -g @angular/cli

ستحتاج أيضًا إلى حساب مطور طبقة التجارة. باستخدام حساب المطور ، ستحتاج إلى إنشاء مؤسسة اختبارية وبثها ببيانات الاختبار. يُسهل Seeding تطوير التطبيق أولاً دون القلق بشأن البيانات التي سيتعين عليك استخدامها. يمكنك إنشاء حساب على هذا الرابط ومنظمة هنا.

لوحة معلومات مؤسسات حساب مطور طبقة التجارة
لوحة معلومات مؤسسات حساب مطور طبقة التجارة حيث يمكنك إضافة مؤسستك. (معاينة كبيرة)
نموذج إنشاء مؤسسات طبقة التجارة
حدد المربع "البذور مع اختبار البيانات" عند إنشاء مؤسسة جديدة. (معاينة كبيرة)

أخيرًا ، ستحتاج إلى حساب Paypal Sandbox. سيسمح لنا امتلاك هذا النوع من الحسابات باختبار المعاملات بين الشركات والمستخدمين دون المخاطرة بالمال الفعلي. يمكنك إنشاء واحدة هنا. يحتوي حساب وضع الحماية على عمل تجريبي واختبار الحساب الشخصي الذي تم إنشاؤه بالفعل له.

المزيد بعد القفز! أكمل القراءة أدناه ↓

طبقة التجارة وتكوين باي بال

لجعل مدفوعات Paypal Sandbox ممكنة على Commerce Layer ، ستحتاج إلى إعداد مفاتيح API. توجه إلى نظرة عامة على الحسابات الخاصة بحساب مطور Paypal الخاص بك. حدد حسابًا تجاريًا وضمن علامة تبويب بيانات اعتماد API الخاصة بتفاصيل الحساب ، ستجد التطبيق الافتراضي ضمن تطبيقات REST .

علامة تبويب بيانات اعتماد API في النافذة المنبثقة لتفاصيل حساب الأعمال في Paypal Sandbox
أين يمكن العثور على تطبيق REST الافتراضي في النافذة المنبثقة لتفاصيل حساب الأعمال على Paypal. (معاينة كبيرة)
نظرة عامة على التطبيق الافتراضي لإعدادات حساب الأعمال في Paypal Sandbox
نظرة عامة على التطبيق الافتراضي لإعدادات حساب الأعمال في Paypal Sandbox حيث يمكنك الحصول على معرّف وسر عميل REST API. (معاينة كبيرة)

لربط حسابك التجاري على Paypal بمؤسسة Commerce Layer ، انتقل إلى لوحة معلومات مؤسستك. هنا ستضيف بوابة دفع Paypal وطريقة دفع Paypal لأسواقك المختلفة. ضمن الإعدادات> المدفوعات ، حدد بوابات الدفع> Paypal وأضف معرف عميل Paypal وسرًا.

لوحة تحكم بوابة المدفوعات الجديدة على طبقة التجارة
حيث يمكنك إنشاء بوابة مدفوعات Paypal على لوحة معلومات Commerce Layer. (معاينة كبيرة)

بعد إنشاء البوابة ، ستحتاج إلى إنشاء طريقة دفع Paypal لكل سوق تستهدفه لجعل Paypal متاحًا كخيار. ستقوم بذلك ضمن الإعدادات> المدفوعات> طرق الدفع> طريقة الدفع الجديدة .

لوحة تحكم طرق الدفع في طبقة التجارة
حيث يمكنك إنشاء طريقة دفع Paypal في لوحة معلومات Commerce Layer. (معاينة كبيرة)

ملاحظة حول المسارات المستخدمة

توفر طبقة التجارة مسارًا للمصادقة ومجموعة أخرى مختلفة من المسارات لواجهة برمجة التطبيقات الخاصة بهم. يتبادل مسار المصادقة /oauth/token بيانات الاعتماد لرمز مميز. هذا الرمز مطلوب للوصول إلى واجهة برمجة التطبيقات الخاصة بهم. تأخذ بقية مسارات API النمط /api/:resource .

يغطي نطاق هذه المقالة فقط الجزء الأمامي من هذا التطبيق. اخترت تخزين جانب الخادم المميز ، واستخدام الجلسات لتتبع الملكية ، وتقديم ملفات تعريف ارتباط http فقط مع معرف جلسة إلى العميل. لن يتم تناول هذا هنا لأنه خارج نطاق هذه المقالة. ومع ذلك ، تظل المسارات كما هي وتتوافق تمامًا مع Commerce Layer API. على الرغم من وجود مسارين مخصصين غير متاحين من Commerce Layer API التي سنستخدمها. هذه تتعامل بشكل أساسي مع إدارة الجلسة. سوف أشير إلى هذه عندما نصل إليهم ووصف كيف يمكنك تحقيق نتيجة مماثلة.

هناك تناقض آخر قد تلاحظه وهو أن هيئات الطلب تختلف عما تتطلبه Commerce Layer API. نظرًا لأن الطلبات يتم تمريرها إلى خادم آخر لتتم تعبئتها برمز مميز ، فقد صنعت الهيئات بشكل مختلف. كان هذا لتسهيل إرسال الطلبات. في حالة وجود أي تناقضات في هيئات الطلب ، سيتم الإشارة إلى ذلك في الخدمات.

نظرًا لأن هذا خارج النطاق ، فسيتعين عليك تحديد كيفية تخزين الرموز المميزة بأمان. ستحتاج أيضًا إلى تعديل طفيف لهيئات الطلب لتطابق تمامًا ما تتطلبه واجهة برمجة تطبيقات Commerce Layer. عندما يكون هناك تناقض ، سأقوم بالربط بمرجع API وأدلة توضح بالتفصيل كيفية هيكلة الجسم بشكل صحيح.

هيكل التطبيق

لتنظيم التطبيق ، سنقسمه إلى أربعة أجزاء رئيسية. يتم تقديم وصف أفضل لما تفعله كل وحدة ضمن الأقسام المقابلة لها:

  1. الوحدة الأساسية ،
  2. وحدة البيانات ،
  3. الوحدة المشتركة ،
  4. وحدات الميزة.

ستجمع وحدات الميزات الصفحات والمكونات ذات الصلة معًا. سيكون هناك أربع وحدات ميزات:

  1. وحدة المصادقة ،
  2. وحدة المنتج ،
  3. وحدة العربة ،
  4. وحدة الخروج.

عندما نصل إلى كل وحدة ، سأشرح الغرض منها وأقسم محتوياتها.

يوجد أدناه شجرة لمجلد src/app ومكان كل وحدة نمطية.

 src ├── app │ ├── core │ ├── data │ ├── features │ │ ├── auth │ │ ├── cart │ │ ├── checkout │ │ └── products └── shared

إنشاء التطبيق وإضافة التبعيات

سنبدأ بإنشاء التطبيق. ستطلق على منظمتنا اسم The LIme Brand وستحصل على بيانات اختبار مصنفة بالفعل بواسطة Commerce Layer.

 ng new lime-app

سنحتاج إلى بعض التبعيات. مادة زاوية بشكل رئيسي وحتى تدميرها. ستوفر المواد الزاوي المكونات والتصميم. حتى يقوم التدمير تلقائيًا بإلغاء الاشتراك من العناصر التي يمكن ملاحظتها عند تدمير المكونات. لتثبيتها قم بتشغيل:

 npm install @ngneat/until-destroy ng add @angular/material

أصول

عند إضافة عناوين إلى Commerce Layer ، يجب استخدام رمز البلد alpha-2. سنضيف ملف json يحتوي على هذه الرموز إلى مجلد assets في assets/json/country-codes.json . يمكنك العثور على هذا الملف مرتبط هنا.

الأنماط

المكونات التي سننشئها تشترك في بعض التصميم العالمي. سنضعها في styles.css التي يمكن العثور عليها على هذا الرابط.

بيئة

سيتألف تكويننا من حقلين. apiUrl الذي يجب أن يشير إلى Commerce Layer API. يتم استخدام apiUrl بواسطة الخدمات التي سننشئها لجلب البيانات. يجب أن يكون clientUrl هو المجال الذي يعمل عليه التطبيق. نستخدم هذا عند تعيين عناوين URL لإعادة التوجيه لـ Paypal. يمكنك أن تجد هذا الملف على هذا الرابط.

وحدة مشتركة

ستحتوي الوحدة المشتركة على الخدمات والأنابيب والمكونات المشتركة عبر الوحدات النمطية الأخرى.

 ng gm shared

وتتكون من ثلاثة مكونات ، وأنبوب واحد ، وخدمتين. هذا ما سيبدو عليه ذلك.

 src/app/shared ├── components │ ├── item-quantity │ │ ├── item-quantity.component.css │ │ ├── item-quantity.component.html │ │ └── item-quantity.component.ts │ ├── simple-page │ │ ├── simple-page.component.css │ │ ├── simple-page.component.html │ │ └── simple-page.component.ts │ └── title │ ├── title.component.css │ ├── title.component.html │ └── title.component.ts ├── pipes │ └── word-wrap.pipe.ts ├── services │ ├── http-error-handler.service.ts │ └── local-storage.service.ts └── shared.module.ts

سنستخدم أيضًا الوحدة المشتركة لتصدير بعض مكونات المواد الزاويّة شائعة الاستخدام. هذا يجعل من السهل استخدامها خارج الصندوق بدلاً من استيراد كل مكون عبر وحدات مختلفة. إليك ما سيحتوي على shared.module.ts .

 @NgModule({ declarations: [SimplePageComponent, TitleComponent, WordWrapPipe, ItemQuantityComponent], imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, RouterModule], exports: [ CommonModule, ItemQuantityComponent, MatButtonModule, MatIconModule, MatSnackBarModule, MatTooltipModule, SimplePageComponent, TitleComponent, WordWrapPipe ] }) export class SharedModule { }

عناصر

عنصر كمية العنصر

يحدد هذا المكون كمية العناصر عند إضافتها إلى سلة التسوق. سيتم استخدامه في عربة التسوق ووحدات المنتجات. كان من الممكن أن يكون اختيار المواد اختيارًا سهلاً لهذا الغرض. ومع ذلك ، فإن نمط المادة المختارة لا يتطابق مع مدخلات المواد المستخدمة في جميع الأشكال الأخرى. بدت قائمة المواد مشابهة جدًا لمدخلات المواد المستخدمة. لذلك قررت إنشاء مكون محدد معه بدلاً من ذلك.

 ng gc shared/components/item-quantity

سيحتوي المكون على ثلاث خصائص إدخال وخاصية إخراج واحدة. تحدد quantity الكمية الأولية للعناصر ، وتشير maxValue إلى الحد الأقصى لعدد العناصر التي يمكن تحديدها دفعة واحدة ، بينما يشير disabled إلى ما إذا كان يجب تعطيل المكون أم لا. يتم تشغيل setQuantityEvent عند تحديد كمية.

عند تهيئة المكون ، سنقوم بتعيين القيم التي تظهر في قائمة المواد. توجد أيضًا طريقة تسمى setQuantity ستبعث أحداث setQuantityEvent .

هذا هو ملف المكون.

 @Component({ selector: 'app-item-quantity', templateUrl: './item-quantity.component.html', styleUrls: ['./item-quantity.component.css'] }) export class ItemQuantityComponent implements OnInit { @Input() quantity: number = 0; @Input() maxValue?: number = 0; @Input() disabled?: boolean = false; @Output() setQuantityEvent = new EventEmitter<number>(); values: number[] = []; constructor() { } ngOnInit() { if (this.maxValue) { for (let i = 1; i <= this.maxValue; i++) { this.values.push(i); } } } setQuantity(value: number) { this.setQuantityEvent.emit(value); } }

هذا هو قالبها.

 <button mat-stroked-button [matMenuTriggerFor]="menu" [disabled]="disabled"> {{quantity}} <mat-icon *ngIf="!disabled">expand_more</mat-icon> </button> <mat-menu #menu="matMenu"> <button *ngFor="let no of values" (click)="setQuantity(no)" mat-menu-item>{{no}}</button> </mat-menu>

هنا هو أسلوبها.

 button { margin: 3px; }

مكون العنوان

يتضاعف هذا المكون كعنوان متدرج بالإضافة إلى عنوان عادي في بعض الصفحات الأبسط. على الرغم من أن Angular Material توفر مكونًا متدرجًا ، إلا أنها لم تكن الأنسب لعملية سحب طويلة إلى حد ما ، ولم تكن متجاوبة مع شاشات العرض الأصغر ، وتطلبت الكثير من الوقت للتنفيذ. ومع ذلك ، يمكن إعادة توجيه العنوان الأبسط كمؤشر متدرج ويكون مفيدًا عبر صفحات متعددة.

 ng gc shared/components/title

يحتوي المكون على أربع خصائص إدخال: title ، وعنوان subtitle ، ورقم ( no ) ، ونص centerText ، للإشارة إلى ما إذا كان سيتم توسيط نص المكون.

 @Component({ selector: 'app-title', templateUrl: './title.component.html', styleUrls: ['./title.component.css'] }) export class TitleComponent { @Input() title: string = ''; @Input() subtitle: string = ''; @Input() no?: string; @Input() centerText?: boolean = false; }

يوجد أدناه نموذجها. يمكنك أن تجد تصميمها مرتبطًا هنا.

 <div> <h1 *ngIf="no" class="mat-display-1">{{no}}</h1> <div [ngClass]="{ 'centered-section': centerText}"> <h1 class="mat-display-2">{{title}}</h1> <p>{{subtitle}}</p> </div> </div>

مكون صفحة بسيط

هناك العديد من الحالات التي يكون فيها العنوان والأيقونة والزر كل ما يلزم لصفحة. وهي تتضمن صفحة 404 وصفحة سلة فارغة وصفحة خطأ وصفحة دفع وصفحة وضع الطلب. هذا هو الغرض الذي يخدمه مكون الصفحة البسيط. عند النقر على الزر الموجود في الصفحة ، سيعيد التوجيه إما إلى مسار أو تنفيذ بعض الإجراءات استجابةً buttonEvent .

لصنعه:

 ng gc shared/components/simple-page

هذا هو ملف المكون الخاص به.

 @Component({ selector: 'app-simple-page', templateUrl: './simple-page.component.html', styleUrls: ['./simple-page.component.css'] }) export class SimplePageComponent { @Input() title: string = ''; @Input() subtitle?: string; @Input() number?: string; @Input() icon?: string; @Input() buttonText: string = ''; @Input() centerText?: boolean = false; @Input() buttonDisabled?: boolean = false; @Input() route?: string | undefined; @Output() buttonEvent = new EventEmitter(); constructor(private router: Router) { } buttonClicked() { if (this.route) { this.router.navigateByUrl(this.route); } else { this.buttonEvent.emit(); } } }

ويحتوي قالبه على:

 <div> <app-title no="{{number}}" title="{{title}}" subtitle="{{subtitle}}" [centerText]="centerText"></app-title> <div *ngIf="icon"> <mat-icon color="primary" class="icon">{{icon}}</mat-icon> </div> <button mat-flat-button color="primary" (click)="buttonClicked()" [disabled]="buttonDisabled"> {{buttonText}} </button> </div>

يمكن العثور على التصميم هنا.

أنابيب

كلمة التفاف الأنابيب

أسماء بعض المنتجات وأنواع المعلومات الأخرى المعروضة على الموقع طويلة حقًا. في بعض الحالات ، يعد التفاف هذه الجمل الطويلة في مكونات مادية أمرًا صعبًا. لذلك سنستخدم هذا الأنبوب لتقطيع الجمل إلى طول محدد وإضافة علامات الحذف في نهاية النتيجة.

لإنشائه ، قم بتشغيل:

 ng g pipe shared/pipes/word-wrap

سوف تحتوي على:

 import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'wordWrap' }) export class WordWrapPipe implements PipeTransform { transform(value: string, length: number): string { return `${value.substring(0, length)}...`; } }

خدمات

خدمة معالج خطأ HTTP

يوجد عدد كبير من خدمات http في هذا المشروع. يعد إنشاء معالج الأخطاء لكل طريقة أمرًا متكررًا. لذا فإن إنشاء معالج واحد يمكن استخدامه بواسطة جميع الطرق أمر منطقي. يمكن استخدام معالج الأخطاء لتنسيق خطأ وأيضًا تمرير الأخطاء إلى منصات تسجيل خارجية أخرى.

قم بإنشائه عن طريق تشغيل:

 ng gs shared/services/http-error-handler

ستحتوي هذه الخدمة على طريقة واحدة فقط. ستعمل الطريقة على تنسيق رسالة الخطأ ليتم عرضها بناءً على ما إذا كان خطأ في الخادم أو العميل. ومع ذلك ، هناك مجال لمزيد من التحسين.

 @Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } } @Injectable({ providedIn: 'root' }) export class HttpErrorHandler { constructor() { } handleError(err: HttpErrorResponse): Observable { let displayMessage = ''; if (err.error instanceof ErrorEvent) { displayMessage = `Client-side error: ${err.error.message}`; } else { displayMessage = `Server-side error: ${err.message}`; } return throwError(displayMessage); } }

خدمة التخزين المحلية

سنستخدم التخزين المحلي لتتبع عدد العناصر في عربة التسوق. من المفيد أيضًا تخزين معرف الطلب هنا. الطلب يتوافق مع عربة التسوق في طبقة التجارة.

لإنشاء خدمة التخزين المحلية ، قم بما يلي:

 ng gs shared/services/local-storage

ستحتوي الخدمة على أربع طرق لإضافة وحذف والحصول على العناصر من التخزين المحلي وطريقة أخرى لمسحها.

 import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class LocalStorageService { constructor() { } addItem(key: string, value: string) { localStorage.setItem(key, value); } deleteItem(key: string) { localStorage.removeItem(key); } getItem(key: string): string | null { return localStorage.getItem(key); } clear() { localStorage.clear(); } }

وحدة البيانات

هذه الوحدة مسؤولة عن استرجاع البيانات وإدارتها. إنه ما سنستخدمه للحصول على البيانات التي يستهلكها تطبيقنا. أدناه هيكلها:

 src/app/data ├── data.module.ts ├── models └── services

لإنشاء تشغيل الوحدة:

 ng gm data

عارضات ازياء

تحدد النماذج كيفية هيكلة البيانات التي نستهلكها من واجهة برمجة التطبيقات. سيكون لدينا 16 إعلان للواجهة. لإنشائها قم بتشغيل:

 for model in \ address cart country customer-address \ customer delivery-lead-time line-item order \ payment-method payment-source paypal-payment \ price shipment shipping-method sku stock-location; \ do ng g interface "data/models/${model}"; done

يرتبط الجدول التالي بكل ملف ويقدم وصفًا لماهية كل واجهة.

واجهه المستخدم وصف
عنوان يمثل عنوان عام.
عربة التسوق إصدار من جانب العميل لطلب يتتبع عدد المنتجات التي ينوي العميل شرائها.
دولة رمز البلد Alpha-2.
عنوان العميل عنوان مرتبط بالعميل.
عميل مستخدم مسجل.
مهلة التسليم يمثل مقدار الوقت المستغرق لتسليم الشحنة.
خط سلعة منتج مفصل يضاف إلى سلة التسوق.
ترتيب سلة تسوق أو مجموعة من العناصر.
طريقة الدفع او السداد نوع الدفع المتاح للأمر.
مصدر الدفع دفعة مرتبطة بأمر.
الدفع باي بال دفعة تتم من خلال Paypal
السعر السعر المرتبط بـ SKU.
شحنة مجموعة من العناصر التي يتم شحنها معًا.
طريقة الشحن الطريقة التي يتم من خلالها شحن الطرد.
SKU وحدة فريدة لحفظ المخزون.
موقع المخزون الموقع الذي يحتوي على مخزون SKU.

خدمات

يحتوي هذا المجلد على الخدمات التي تقوم بإنشاء واسترداد ومعالجة بيانات التطبيق. سننشئ 11 خدمة هنا.

 for service in \ address cart country customer-address \ customer delivery-lead-time line-item \ order paypal-payment shipment sku; \ do ng gs "data/services/${service}"; done

خدمة العنوان

هذه الخدمة تنشئ وتسترجع العناوين. من المهم عند إنشاء وتعيين عناوين الشحن والفواتير للطلبات. لها طريقتان. واحد لإنشاء عنوان وآخر لاسترداد واحد.

المسار المستخدم هنا هو /api/addresses . إذا كنت ستستخدم Commerce Layer API مباشرةً ، فتأكد من هيكلة البيانات كما هو موضح في هذا المثال.

 @Injectable({ providedIn: 'root' }) export class AddressService { private url: string = `${environment.apiUrl}/api/addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createAddress(address: Address): Observable<Address> { return this.http.post<Address>(this.url, address) .pipe(catchError(this.eh.handleError)); } getAddress(id: string): Observable<Address> { return this.http.get<Address>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

خدمة عربة التسوق

سلة التسوق مسؤولة عن الحفاظ على كمية العناصر المضافة ومعرف الطلب. قد يكون إجراء مكالمات واجهة برمجة التطبيقات للحصول على عدد العناصر في أمر ما في كل مرة يتم فيها إنشاء عنصر سطر جديد مكلفًا. بدلاً من ذلك ، يمكننا فقط استخدام التخزين المحلي للحفاظ على الاعتماد على العميل. هذا يلغي الحاجة إلى إجراء عمليات جلب غير ضرورية للطلب في كل مرة يتم فيها إضافة عنصر إلى عربة التسوق.

نستخدم هذه الخدمة أيضًا لتخزين معرف الطلب. سلة التسوق تتوافق مع طلب على Commerce Layer. بمجرد إضافة العنصر الأول إلى سلة التسوق ، يتم إنشاء طلب. نحتاج إلى الاحتفاظ بمعرف الطلب هذا حتى نتمكن من جلبه أثناء عملية السداد.

بالإضافة إلى ذلك ، نحتاج إلى طريقة لإبلاغ العنوان بأنه تمت إضافة عنصر إلى سلة التسوق. يحتوي العنوان على زر عربة التسوق ويعرض كمية العناصر الموجودة فيه. سنستخدم موضوع BehaviorSubject يمكن ملاحظته بالقيمة الحالية لعربة التسوق. يمكن للرأس الاشتراك في هذا وتتبع التغييرات في قيمة سلة التسوق.

أخيرًا ، بمجرد اكتمال الطلب ، يجب تصفية قيمة سلة التسوق. هذا يضمن عدم وجود التباس عند إنشاء طلبات أحدث لاحقة. يتم مسح القيم التي تم تخزينها بمجرد وضع علامة على الترتيب الحالي على أنه موضوع.

سننجز كل هذا باستخدام خدمة التخزين المحلية التي تم إنشاؤها مسبقًا.

 @Injectable({ providedIn: 'root' }) export class CartService { private cart = new BehaviorSubject({ orderId: this.orderId, itemCount: this.itemCount }); cartValue$ = this.cart.asObservable(); constructor(private storage: LocalStorageService) { } get orderId(): string { const id = this.storage.getItem('order-id'); return id ? id : ''; } set orderId(id: string) { this.storage.addItem('order-id', id); this.cart.next({ orderId: id, itemCount: this.itemCount }); } get itemCount(): number { const itemCount = this.storage.getItem('item-count'); return itemCount ? parseInt(itemCount) : 0; } set itemCount(amount: number) { this.storage.addItem('item-count', amount.toString()); this.cart.next({ orderId: this.orderId, itemCount: amount }); } incrementItemCount(amount: number) { this.itemCount = this.itemCount + amount; } decrementItemCount(amount: number) { this.itemCount = this.itemCount - amount; } clearCart() { this.storage.deleteItem('item-count'); this.cart.next({ orderId: '', itemCount: 0 }); } }

خدمة الدولة

عند إضافة عناوين على Commerce Layer ، يجب أن يكون رمز البلد هو رمز alpha 2. تقرأ هذه الخدمة ملف json يحتوي على هذه الرموز لكل بلد وتعيده في طريقة getCountries الخاصة بها.

 @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } } @Injectable({ providedIn: 'root' }) export class CountryService { constructor(private http: HttpClient) { } getCountries(): Observable { return this.http.get ('./../../../assets/json/country-codes.json'); } }

خدمة عنوان العميل

تُستخدم هذه الخدمة لربط العناوين بالعملاء. كما أنه يجلب عنوانًا محددًا أو كل العناوين المتعلقة بالعميل. يتم استخدامه عندما يضيف العميل عناوين الشحن والفواتير إلى طلباتهم. تقوم طريقة createCustomer بإنشاء عميل ، وتحصل getCustomerAddresses على جميع عناوين العميل ، وتحصل getCustomerAddress على عنوان محدد.

عند إنشاء عنوان عميل ، تأكد من هيكلة نص المنشور وفقًا لهذا المثال.

 @Injectable({ providedIn: 'root' }) export class CustomerAddressService { private url: string = `${environment.apiUrl}/api/customer_addresses`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomerAddress(addressId: string, customerId: string): Observable<CustomerAddress> { return this.http.post<CustomerAddress>(this.url, { addressId: addressId, customerId: customerId }) .pipe(catchError(this.eh.handleError)); } getCustomerAddresses(): Observable<CustomerAddress[]> { return this.http.get<CustomerAddress[]>(`${this.url}`) .pipe(catchError(this.eh.handleError)); } getCustomerAddress(id: string): Observable<CustomerAddress> { return this.http.get<CustomerAddress>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

خدمة العملاء

يتم إنشاء العملاء واسترجاع معلوماتهم باستخدام هذه الخدمة. عندما يقوم المستخدم بالتسجيل ، يصبح عميلاً ويتم إنشاؤه باستخدام طريقة createCustomerMethod . getCustomer بإرجاع العميل المرتبط بمعرف معين. getCurrentCustomer بإرجاع العميل الذي قام بتسجيل الدخول حاليًا.

عند إنشاء عميل ، قم ببناء البيانات مثل هذا. يمكنك إضافة الاسم الأول والأخير إلى البيانات الوصفية ، كما هو موضح في سماتها.

المسار /api/customers/current غير متاح في طبقة التجارة. لذلك ستحتاج إلى معرفة كيفية الحصول على العميل الذي قام بتسجيل الدخول حاليًا.

 @Injectable({ providedIn: 'root' }) export class CustomerService { private url: string = `${environment.apiUrl}/api/customers`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createCustomer(email: string, password: string, firstName: string, lastName: string): Observable<Customer> { return this.http.post<Customer>(this.url, { email: email, password: password, firstName: firstName, lastName: lastName }) .pipe(catchError(this.eh.handleError)); } getCurrentCustomer(): Observable<Customer> { return this.http.get<Customer>(`${this.url}/current`) .pipe(catchError(this.eh.handleError)); } getCustomer(id: string): Observable<Customer> { return this.http.get<Customer>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

خدمة التسليم في الوقت المحدد

تقوم هذه الخدمة بإرجاع معلومات حول الجداول الزمنية للشحن من مواقع المخزون المختلفة.

 @Injectable({ providedIn: 'root' }) export class DeliveryLeadTimeService { private url: string = `${environment.apiUrl}/api/delivery_lead_times`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getDeliveryLeadTimes(): Observable<DeliveryLeadTime[]> { return this.http.get<DeliveryLeadTime[]>(this.url) .pipe(catchError(this.eh.handleError)); } }

خدمة البند

العناصر المضافة إلى عربة التسوق تديرها هذه الخدمة. باستخدامه ، يمكنك إنشاء عنصر لحظة إضافته إلى سلة التسوق. يمكن أيضًا جلب معلومات العنصر. يمكن أيضًا تحديث العنصر عند تغيير كميته أو حذفه عند إزالته من سلة التسوق.

عند إنشاء العناصر أو تحديثها ، قم بهيكلة نص الطلب كما هو موضح في هذا المثال.

 @Injectable({ providedIn: 'root' }) export class LineItemService { private url: string = `${environment.apiUrl}/api/line_items`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createLineItem(lineItem: LineItem): Observable<LineItem> { return this.http.post<LineItem>(this.url, lineItem) .pipe(catchError(this.eh.handleError)); } getLineItem(id: string): Observable<LineItem> { return this.http.get<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateLineItem(id: string, quantity: number): Observable<LineItem> { return this.http.patch<LineItem>(`${this.url}/${id}`, { quantity: quantity }) .pipe(catchError(this.eh.handleError)); } deleteLineItem(id: string): Observable<LineItem> { return this.http.delete<LineItem>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } }

طلب الخدمة

على غرار خدمة البند ، تتيح لك خدمة الطلب إنشاء طلب أو تحديثه أو حذفه أو الحصول عليه. بالإضافة إلى ذلك ، يمكنك اختيار الحصول على الشحنات المرتبطة بأمر ما بشكل منفصل باستخدام طريقة getOrderShipments . يتم استخدام هذه الخدمة بكثافة طوال عملية الخروج.

هناك أنواع مختلفة من المعلومات حول الطلب والتي تكون مطلوبة طوال عملية الدفع. نظرًا لأنه قد يكون من المكلف إحضار طلب كامل وعلاقاته ، فإننا نحدد ما نريد الحصول عليه من أمر باستخدام GetOrderParams . المكافئ لهذا في CL API هو معلمة استعلام التضمين حيث تقوم بإدراج علاقات الطلب المراد تضمينها. يمكنك التحقق من الحقول التي يجب تضمينها في ملخص سلة التسوق هنا ولمراحل الخروج المختلفة هنا.

بنفس الطريقة ، عند تحديث أحد الطلبات ، نستخدم UpdateOrderParams لتحديد حقول التحديث. هذا لأنه في الخادم الذي يملأ الرمز المميز ، يتم تنفيذ بعض العمليات الإضافية اعتمادًا على الحقل الذي يتم تحديثه. ومع ذلك ، إذا كنت تقدم طلبات مباشرة إلى CL API ، فلن تحتاج إلى تحديد ذلك. يمكنك التخلص منها لأن CL API لا تتطلب منك تحديدها. على الرغم من أن نص الطلب يجب أن يشبه هذا المثال.

 @Injectable({ providedIn: 'root' }) export class OrderService { private url: string = `${environment.apiUrl}/api/orders`; constructor( private http: HttpClient, private eh: HttpErrorHandler) { } createOrder(): Observable<Order> { return this.http.post<Order>(this.url, {}) .pipe(catchError(this.eh.handleError)); } getOrder(id: string, orderParam: GetOrderParams): Observable<Order> { let params = {}; if (orderParam != GetOrderParams.none) { params = { [orderParam]: 'true' }; } return this.http.get<Order>(`${this.url}/${id}`, { params: params }) .pipe(catchError(this.eh.handleError)); } updateOrder(order: Order, params: UpdateOrderParams[]): Observable<Order> { let updateParams = []; for (const param of params) { updateParams.push(param.toString()); } return this.http.patch<Order>( `${this.url}/${order.id}`, order, { params: { 'field': updateParams } } ) .pipe(catchError(this.eh.handleError)); } getOrderShipments(id: string): Observable<Shipment[]> { return this.http.get<Shipment[]>(`${this.url}/${id}/shipments`) .pipe(catchError(this.eh.handleError)); } }

خدمة الدفع باي بال

هذه الخدمة مسؤولة عن إنشاء وتحديث مدفوعات Paypal للطلبات. بالإضافة إلى ذلك ، يمكننا الحصول على مدفوعات Paypal وفقًا لمعرفها. يجب أن يحتوي نص المنشور على هيكل مشابه لهذا المثال عند إنشاء دفعة Paypal.

 @Injectable({ providedIn: 'root' }) export class PaypalPaymentService { private url: string = `${environment.apiUrl}/api/paypal_payments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } createPaypalPayment(payment: PaypalPayment): Observable<PaypalPayment> { return this.http.post<PaypalPayment>(this.url, payment) .pipe(catchError(this.eh.handleError)); } getPaypalPayment(id: string): Observable<PaypalPayment> { return this.http.get<PaypalPayment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updatePaypalPayment(id: string, paypalPayerId: string): Observable<PaypalPayment> { return this.http.patch<PaypalPayment>( `${this.url}/${id}`, { paypalPayerId: paypalPayerId } ) .pipe(catchError(this.eh.handleError)); } }

خدمة الشحن

تحصل هذه الخدمة على شحنة أو تقوم بتحديثها من خلال معرفتها. يجب أن يبدو نص طلب تحديث الشحنة مشابهًا لهذا المثال.

 @Injectable({ providedIn: 'root' }) export class ShipmentService { private url: string = `${environment.apiUrl}/api/shipments`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getShipment(id: string): Observable<Shipment> { return this.http.get<Shipment>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } updateShipment(id: string, shippingMethodId: string): Observable<Shipment> { return this.http.patch<Shipment>( `${this.url}/${id}`, { shippingMethodId: shippingMethodId } ) .pipe(catchError(this.eh.handleError)); } }

خدمة SKU

تحصل خدمة SKU على المنتجات من المتجر. إذا تم استرداد العديد من المنتجات ، فيمكن ترقيمها وتعيين حجم صفحة لها. يجب تعيين حجم الصفحة ورقم الصفحة كمعلمات استعلام كما في هذا المثال إذا كنت تقدم طلبات مباشرة إلى واجهة برمجة التطبيقات. يمكن أيضًا استرداد منتج واحد من خلال معرفته.

 @Injectable({ providedIn: 'root' }) export class SkuService { private url: string = `${environment.apiUrl}/api/skus`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getSku(id: string): Observable<Sku> { return this.http.get<Sku>(`${this.url}/${id}`) .pipe(catchError(this.eh.handleError)); } getSkus(page: number, pageSize: number): Observable<Sku[]> { return this.http.get<Sku[]>( this.url, { params: { 'page': page.toString(), 'pageSize': pageSize.toString() } }) .pipe(catchError(this.eh.handleError)); } }

الوحدة الأساسية

تحتوي الوحدة الأساسية على كل شيء أساسي ومشترك عبر التطبيق. يتضمن ذلك مكونات مثل الرأس وصفحات مثل صفحة 404. تندرج هنا أيضًا الخدمات المسؤولة عن المصادقة وإدارة الجلسة ، بالإضافة إلى أدوات الاعتراض والحراس على مستوى التطبيق.

ستبدو شجرة الوحدة الأساسية هكذا.

 src/app/core ├── components │ ├── error │ │ ├── error.component.css │ │ ├── error.component.html │ │ └── error.component.ts │ ├── header │ │ ├── header.component.css │ │ ├── header.component.html │ │ └── header.component.ts │ └── not-found │ ├── not-found.component.css │ ├── not-found.component.html │ └── not-found.component.ts ├── core.module.ts ├── guards │ └── empty-cart.guard.ts ├── interceptors │ └── options.interceptor.ts └── services ├── authentication.service.ts ├── header.service.ts └── session.service.ts

لإنشاء الوحدة ومحتوياتها قم بتشغيل:

 ng gm core ng gg core/guards/empty-cart ng gs core/header/header ng g interceptor core/interceptors/options for comp in header error not-found; do ng gc "core/${comp}"; done for serv in authentication session; do ng gs "core/authentication/${serv}"; done

يجب أن يعجب ملف الوحدة الأساسية بهذا. لاحظ أنه تم تسجيل المسارات لـ NotFoundComponent و ErrorComponent .

 @NgModule({ declarations: [HeaderComponent, NotFoundComponent, ErrorComponent], imports: [ RouterModule.forChild([ { path: '404', component: NotFoundComponent }, { path: 'error', component: ErrorComponent }, { path: '**', redirectTo: '/404' } ]), MatBadgeModule, SharedModule ], exports: [HeaderComponent] }) export class CoreModule { }

خدمات

يحتوي مجلد الخدمات على خدمات المصادقة والجلسة والرأس.

خدمة المصادقة

تتيح لك خدمة AuthenticationService الحصول على الرموز المميزة للعميل والعميل. تُستخدم هذه الرموز المميزة للوصول إلى بقية مسارات واجهة برمجة التطبيقات. يتم إرجاع الرموز المميزة للعميل عندما يتبادل المستخدم بريدًا إلكترونيًا وكلمة مرور له ولديه نطاق أوسع من الأذونات. يتم إصدار الرموز المميزة للعميل دون الحاجة إلى بيانات اعتماد ولها أذونات أضيق.

يحصل getClientSession على رمز عميل. login يحصل على رمز العميل. كلا الطريقتين أيضا إنشاء جلسة. يجب أن يبدو نص طلب الرمز المميز للعميل مثل هذا وجسم الرمز المميز للعميل مثل هذا.

 @Injectable({ providedIn: 'root' }) export class AuthenticationService { private url: string = `${environment.apiUrl}/oauth/token`; constructor(private http: HttpClient, private eh: HttpErrorHandler) { } getClientSession(): Observable<object> { return this.http.post<object>( this.url, { grantType: 'client_credentials' }) .pipe(catchError(this.eh.handleError)); } login(email: string, password: string): Observable<object> { return this.http.post<object>( this.url, { username: email, password: password, grantType: 'password' }) .pipe(catchError(this.eh.handleError)); } }

Session Service

The SessionService is responsible for session management. The service will contain an observable from a BehaviorSubject called loggedInStatus to communicate whether a user is logged in. setLoggedInStatus sets the value of this subject, true for logged in, and false for not logged in. isCustomerLoggedIn makes a request to the server to check if the user has an existing session. logout destroys the session on the server. The last two methods access routes that are unique to the server that populates the request with a token. They are not available from Commerce Layer. You'll have to figure out how to implement them.

 @Injectable({ providedIn: 'root' }) export class SessionService { private url: string = `${environment.apiUrl}/session`; private isLoggedIn = new BehaviorSubject(false); loggedInStatus = this.isLoggedIn.asObservable(); constructor(private http: HttpClient, private eh: HttpErrorHandler) { } setLoggedInStatus(status: boolean) { this.isLoggedIn.next(status); } isCustomerLoggedIn(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.url}/customer/status`) .pipe(catchError(this.eh.handleError)); } logout(): Observable<{ message: string }> { return this.http.get<{ message: string }>(`${this.url}/destroy`) .pipe(catchError(this.eh.handleError)); } }

Header Service

The HeaderService is used to communicate whether the cart, login, and logout buttons should be shown in the header. These buttons are hidden on the login and signup pages but present on all other pages to prevent confusion. We'll use an observable from a BehaviourSubject called showHeaderButtons that shares this. We'll also have a setHeaderButtonsVisibility method to set this value.

 @Injectable({ providedIn: 'root' }) export class HeaderService { private headerButtonsVisibility = new BehaviorSubject(true); showHeaderButtons = this.headerButtonsVisibility.asObservable(); constructor() { } setHeaderButtonsVisibility(visible: boolean) { this.headerButtonsVisibility.next(visible); } }

عناصر

Error Component

This component is used as an error page. It is useful in instances when server requests fail and absolutely no data is displayed on a page. Instead of showing a blank page, we let the user know that a problem occurred. Below is it's template.

 <app-simple-page title="An error occurred" subtitle="There was a problem fetching your page" buttonText="GO TO HOME" icon="report" [centerText]="true" route="/"> </app-simple-page>

This is what the component will look like.

Screenshot of error page
Screenshot of error page. (معاينة كبيرة)

Not Found Component

This is a 404 page that the user gets redirected to when they request a route not available on the router. Only its template is modified.

 <app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Screenshot of 404 page
Screenshot of 404 page. (معاينة كبيرة)

Header Component

The HeaderComponent is basically the header displayed at the top of a page. It will contain the app title, the cart, login, and logout buttons.

When this component is initialized, a request is made to check whether the user has a current session. This happens when subscribing to this.session.isCustomerLoggedIn() . We subscribe to this.session.loggedInStatus to check if the user logs out throughout the life of the app. The this.header.showHeaderButtons subscription decides whether to show all the buttons on the header or hide them. this.cart.cartValue$ gets the count of items in the cart.

توجد طريقة logout تدمر جلسة المستخدم وتخصص له رمزًا مميزًا للعميل. يتم تعيين رمز مميز للعميل لأن الجلسة التي تحافظ على الرمز المميز للعميل قد تم إتلافها ولا تزال هناك حاجة إلى رمز مميز لكل طلب واجهة برمجة تطبيقات. يقوم شريط الوجبات الخفيفة المادي بإبلاغ المستخدم ما إذا كانت جلسته قد تم إتلافها بنجاح أم لا.

نستخدم @UntilDestroy({ checkProperties: true }) للإشارة إلى أنه يجب إلغاء الاشتراك تلقائيًا عند تدمير المكون.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.css'] }) export class HeaderComponent implements OnInit { cartAmount: number = 0; isLoggedIn: boolean = false; showButtons: boolean = true; constructor( private session: SessionService, private snackBar: MatSnackBar, private cart: CartService, private header: HeaderService, private auth: AuthenticationService ) { } ngOnInit() { this.session.isCustomerLoggedIn() .subscribe( () => { this.isLoggedIn = true; this.session.setLoggedInStatus(true); } ); this.session.loggedInStatus.subscribe(status => this.isLoggedIn = status); this.header.showHeaderButtons.subscribe(visible => this.showButtons = visible); this.cart.cartValue$.subscribe(cart => this.cartAmount = cart.itemCount); } logout() { concat( this.session.logout(), this.auth.getClientSession() ).subscribe( () => { this.snackBar.open('You have been logged out.', 'Close', { duration: 4000 }); this.session.setLoggedInStatus(false); }, err => this.snackBar.open('There was a problem logging you out.', 'Close', { duration: 4000 }) ); } }

يوجد أدناه قالب الرأس والمرتبط هنا بتصميمه.

 <div> <div routerLink="/"> <h1><span>Lime</span><span>Store</span></h1> </div> <div> <div *ngIf="showButtons"> <button mat-icon-button color="primary" aria-label="shopping cart"> <mat-icon [matBadge]="cartAmount" matBadgeColor="accent" aria-label="shopping cart" routerLink="/cart">shopping_cart</mat-icon> </button> <button mat-icon-button color="primary" aria-label="login" *ngIf="!isLoggedIn"> <mat-icon aria-label="login" matTooltip="login" routerLink="/login">login</mat-icon> </button> <button mat-icon-button color="primary" aria-label="logout" *ngIf="isLoggedIn" (click)="logout()"> <mat-icon aria-label="logout" matTooltip="logout">logout</mat-icon> </button> </div> </div> </div>

حراس

حارس العربة الفارغة

يمنع هذا الحارس المستخدمين من الوصول إلى المسارات المتعلقة بالسداد والفواتير إذا كانت عربة التسوق فارغة. هذا لأنه لمتابعة عملية الدفع ، يجب أن يكون هناك أمر صالح. الأمر يتوافق مع عربة بها عناصر. إذا كانت هناك عناصر في عربة التسوق ، فيمكن للمستخدم المتابعة إلى صفحة محمية. ومع ذلك ، إذا كانت عربة التسوق فارغة ، تتم إعادة توجيه المستخدم إلى صفحة عربة التسوق الفارغة.

 @Injectable({ providedIn: 'root' }) export class EmptyCartGuard implements CanActivate { constructor(private cart: CartService, private router: Router) { } canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { if (this.cart.orderId) { if (this.cart.itemCount > 0) { return true; } } return this.router.parseUrl('/empty'); } }

المعترضون

معترض الخيارات

يعترض هذا المعترض جميع طلبات HTTP الصادرة ويضيف خيارين إلى الطلب. هذه هي رأس Content-Type withCredentials . تحدد withCredentials ما إذا كان يجب إرسال طلب مع بيانات اعتماد صادرة مثل ملفات تعريف الارتباط http فقط التي نستخدمها. نستخدم Content-Type للإشارة إلى أننا نرسل موارد json إلى الخادم.

 @Injectable() export class OptionsInterceptor implements HttpInterceptor { constructor() { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { request = request.clone({ headers: request.headers.set('Content-Type', 'application/json'), withCredentials: true }); return next.handle(request); } }

وحدات الميزة

يحتوي هذا القسم على الميزات الرئيسية للتطبيق. كما ذكرنا سابقًا ، يتم تجميع الميزات في أربع وحدات: المصادقة ، والمنتج ، والعربة ، ووحدات الدفع.

وحدة المنتجات

تحتوي وحدة المنتجات على صفحات تعرض المنتجات المعروضة للبيع. وتشمل هذه صفحة المنتج وصفحة قائمة المنتجات. إنه منظم كما هو موضح أدناه.

 src/app/features/products ├── pages │ ├── product │ │ ├── product.component.css │ │ ├── product.component.html │ │ └── product.component.ts │ └── product-list │ ├── product-list.component.css │ ├── product-list.component.html │ └── product-list.component.ts └── products.module.ts

لتوليدها ومكوناتها:

 ng gm features/products ng gc features/products/pages/product ng gc features/products/pages/product-list

هذا هو ملف الوحدة:

 @NgModule({ declarations: [ProductListComponent, ProductComponent], imports: [ RouterModule.forChild([ { path: 'product/:id', component: ProductComponent }, { path: '', component: ProductListComponent } ]), LayoutModule, MatCardModule, MatGridListModule, MatPaginatorModule, SharedModule ] }) export class ProductsModule { }

مكون قائمة المنتجات

يعرض هذا المكون قائمة ذات ترقيم صفحات للمنتجات المتاحة للبيع. إنها الصفحة الأولى التي يتم تحميلها عند بدء تشغيل التطبيق.

يتم عرض المنتجات في شبكة. قائمة شبكة المواد هي أفضل مكون لهذا. لجعل الشبكة تستجيب ، سيتغير عدد أعمدة الشبكة اعتمادًا على حجم الشاشة. تتيح لنا خدمة BreakpointObserver تحديد حجم الشاشة وتعيين الأعمدة أثناء التهيئة.

للحصول على المنتجات ، نسمي طريقة getProducts الخاصة بـ SkuService . تقوم بإرجاع المنتجات إذا نجحت وتعيينها إلى الشبكة. إذا لم يكن كذلك ، فإننا نوجه المستخدم إلى صفحة الخطأ.

نظرًا لأن المنتجات المعروضة مرقمة ، سيكون لدينا طريقة getNextPage للحصول على المنتجات الإضافية.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { cols = 4; length = 0; pageIndex = 0; pageSize = 20; pageSizeOptions: number[] = [5, 10, 20]; pageEvent!: PageEvent | void; products: Sku[] = []; constructor( private breakpointObserver: BreakpointObserver, private skus: SkuService, private router: Router, private header: HeaderService) { } ngOnInit() { this.getProducts(1, 20); this.header.setHeaderButtonsVisibility(true); this.breakpointObserver.observe([ Breakpoints.Handset, Breakpoints.Tablet, Breakpoints.Web ]).subscribe(result => { if (result.matches) { if (result.breakpoints['(max-width: 599.98px) and (orientation: portrait)'] || result.breakpoints['(max-width: 599.98px) and (orientation: landscape)']) { this.cols = 1; } else if (result.breakpoints['(min-width: 1280px) and (orientation: portrait)'] || result.breakpoints['(min-width: 1280px) and (orientation: landscape)']) { this.cols = 4; } else { this.cols = 3; } } }); } private getProducts(page: number, pageSize: number) { this.skus.getSkus(page, pageSize) .subscribe( skus => { this.products = skus; this.length = skus[0].__collectionMeta.recordCount; }, err => this.router.navigateByUrl('/error') ); } getNextPage(event: PageEvent) { this.getProducts(event.pageIndex + 1, event.pageSize); } trackSkus(index: number, item: Sku) { return `${item.id}-${index}`; } }

النموذج موضح أدناه ويمكن العثور على التصميم الخاص به هنا.

 <mat-grid-list cols="{{cols}}" rowHeight="400px" gutterSize="20px" class="grid-layout"> <mat-grid-tile *ngFor="let product of products; trackBy: trackSkus"> <mat-card> <img mat-card-image src="{{product.imageUrl}}" alt="product photo"> <mat-card-content> <mat-card-title matTooltip="{{product.name}}">{{product.name |wordWrap:35}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> </mat-card-content> <mat-card-actions> <button mat-flat-button color="primary" [routerLink]="['/product', product.id]"> View </button> </mat-card-actions> </mat-card> </mat-grid-tile> </mat-grid-list> <mat-paginator [length]="length" [pageIndex]="pageIndex" [pageSize]="pageSize" [pageSizeOptions]="pageSizeOptions" (page)="pageEvent = getNextPage($event)"> </mat-paginator>

ستبدو الصفحة مثل هذا.

لقطة شاشة لصفحة قائمة المنتجات
لقطة شاشة لصفحة قائمة المنتجات. (معاينة كبيرة)

مكون المنتج

بمجرد تحديد منتج من صفحة قائمة المنتجات ، يعرض هذا المكون تفاصيله. يتضمن ذلك الاسم الكامل للمنتج والسعر والوصف. يوجد أيضًا زر لإضافة العنصر إلى عربة المنتج.

عند التهيئة ، نحصل على معرف المنتج من معلمات المسار. باستخدام المعرف ، نحضر المنتج من SkuService .

عندما يضيف المستخدم عنصرًا إلى عربة التسوق ، يتم استدعاء طريقة addItemToCart . في ذلك ، نتحقق مما إذا كان قد تم إنشاء طلب لعربة التسوق بالفعل. إذا لم يكن الأمر كذلك ، فسيتم عمل واحد جديد باستخدام OrderService . بعد ذلك ، يتم إنشاء بند بالترتيب الذي يتوافق مع المنتج. إذا كان طلب سلة التسوق موجودًا بالفعل ، فسيتم إنشاء العنصر فقط. اعتمادًا على حالة الطلبات ، يتم عرض رسالة شريط الوجبات الخفيفة للمستخدم.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-product', templateUrl: './product.component.html', styleUrls: ['./product.component.css'] }) export class ProductComponent implements OnInit { id: string = ''; product!: Sku; quantity: number = 0; constructor( private route: ActivatedRoute, private skus: SkuService, private location: Location, private router: Router, private header: HeaderService, private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.paramMap .pipe( mergeMap(params => { const id = params.get('id') this.id = id ? id : ''; return this.skus.getSku(this.id); }), tap((sku) => { this.product = sku; }) ).subscribe({ error: (err) => this.router.navigateByUrl('/error') }); this.header.setHeaderButtonsVisibility(true); } addItemToCart() { if (this.quantity > 0) { if (this.cart.orderId == '') { this.orders.createOrder() .pipe( mergeMap((order: Order) => { this.cart.orderId = order.id || ''; return this.lineItems.createLineItem({ orderId: order.id, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }); }) ) .subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } else { this.lineItems.createLineItem({ orderId: this.cart.orderId, name: this.product.name, imageUrl: this.product.imageUrl, quantity: this.quantity, skuCode: this.product.code }).subscribe( () => { this.cart.incrementItemCount(this.quantity); this.showSuccessSnackBar(); }, err => this.showErrorSnackBar() ); } } else { this.snackBar.open('Select a quantity greater than 0.', 'Close', { duration: 8000 }); } } setQuantity(no: number) { this.quantity = no; } goBack() { this.location.back(); } private showSuccessSnackBar() { this.snackBar.open('Item successfully added to cart.', 'Close', { duration: 8000 }); } private showErrorSnackBar() { this.snackBar.open('Failed to add your item to the cart.', 'Close', { duration: 8000 }); } }

يكون نموذج ProductComponent كما يلي ويتم ربط تصميمه هنا.

 <div> <mat-card *ngIf="product" class="product-card"> <img mat-card-image src="{{product.imageUrl}}" alt="Photo of a product"> <mat-card-content> <mat-card-title>{{product.name}}</mat-card-title> <mat-card-subtitle>{{product.prices[0].compareAtAmountFloat | currency:'EUR'}}</mat-card-subtitle> <p> {{product.description}} </p> </mat-card-content> <mat-card-actions> <app-item-quantity [quantity]="quantity" [maxValue]="10" (setQuantityEvent)="setQuantity($event)"></app-item-quantity> <button mat-raised-button color="accent" (click)="addItemToCart()"> <mat-icon>add_shopping_cart</mat-icon> Add to cart </button> <button mat-raised-button color="primary" (click)="goBack()"> <mat-icon>storefront</mat-icon> Continue shopping </button> </mat-card-actions> </mat-card> </div>

ستبدو الصفحة مثل هذا.

لقطة شاشة لصفحة المنتج
لقطة شاشة لصفحة المنتج. (معاينة كبيرة)

وحدة المصادقة

تحتوي وحدة المصادقة على صفحات مسؤولة عن المصادقة. وتشمل هذه صفحات تسجيل الدخول والاشتراك. تم تنظيمها على النحو التالي.

 src/app/features/auth/ ├── auth.module.ts └── pages ├── login │ ├── login.component.css │ ├── login.component.html │ └── login.component.ts └── signup ├── signup.component.css ├── signup.component.html └── signup.component.ts

لتوليدها ومكوناتها:

 ng gm features/auth ng gc features/auth/pages/signup ng gc features/auth/pages/login

هذا هو ملف الوحدة الخاصة به.

 @NgModule({ declarations: [LoginComponent, SignupComponent], imports: [ RouterModule.forChild([ { path: 'login', component: LoginComponent }, { path: 'signup', component: SignupComponent } ]), MatFormFieldModule, MatInputModule, ReactiveFormsModule, SharedModule ] }) export class AuthModule { }

مكون الاشتراك

يقوم المستخدم بالتسجيل للحصول على حساب باستخدام هذا المكون. مطلوب الاسم الأول واسم العائلة والبريد الإلكتروني وكلمة المرور لهذه العملية. يحتاج المستخدم أيضًا إلى تأكيد كلمة المرور الخاصة به. سيتم إنشاء حقول الإدخال باستخدام خدمة FormBuilder . تتم إضافة التحقق من الصحة لطلب أن تحتوي جميع المدخلات على قيم. تمت إضافة تحقق إضافي إلى حقل كلمة المرور لضمان حد أدنى للطول يبلغ ثمانية أحرف. يضمن المدقق المخصص matchPasswords أن كلمة المرور المؤكدة تتطابق مع كلمة المرور الأولية.

عند تهيئة المكون ، يتم إخفاء أزرار عربة التسوق وتسجيل الدخول والخروج في الرأس ، ويتم توصيل هذا إلى الرأس باستخدام HeaderService .

بعد وضع علامة على جميع الحقول على أنها صالحة ، يمكن للمستخدم التسجيل. في طريقة signup ، تتلقى طريقة createCustomer الخاصة CustomerService هذا الإدخال. في حالة نجاح التسجيل ، يتم إبلاغ المستخدم بأنه تم إنشاء حسابه بنجاح باستخدام شريط الوجبات الخفيفة. ثم يتم إعادة توجيههم إلى الصفحة الرئيسية.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-signup', templateUrl: './signup.component.html', styleUrls: ['./signup.component.css'] }) export class SignupComponent implements OnInit { signupForm = this.fb.group({ firstName: ['', Validators.required], lastName: ['', Validators.required], email: ['', [Validators.required, Validators.email]], password: ['', [Validators.required, Validators.minLength(8)]], confirmedPassword: ['', [Validators.required]] }, { validators: this.matchPasswords }); @ViewChild(FormGroupDirective) sufDirective: FormGroupDirective | undefined; constructor( private customer: CustomerService, private fb: FormBuilder, private snackBar: MatSnackBar, private router: Router, private header: HeaderService ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } matchPasswords(signupGroup: AbstractControl): ValidationErrors | null { const password = signupGroup.get('password')?.value; const confirmedPassword = signupGroup.get('confirmedPassword')?.value; return password == confirmedPassword ? null : { differentPasswords: true }; } get password() { return this.signupForm.get('password'); } get confirmedPassword() { return this.signupForm.get('confirmedPassword'); } signup() { const customer = this.signupForm.value; this.customer.createCustomer( customer.email, customer.password, customer.firstName, customer.lastName ).subscribe( () => { this.signupForm.reset(); this.sufDirective?.resetForm(); this.snackBar.open('Account successfully created. You will be redirected in 5 seconds.', 'Close', { duration: 5000 }); setTimeout(() => this.router.navigateByUrl('/'), 6000); }, err => this.snackBar.open('There was a problem creating your account.', 'Close', { duration: 5000 }) ); } }

يوجد أدناه نموذج SignupComponent .

 <form [formGroup]="signupForm" (ngSubmit)="signup()"> <h1 class="mat-display-3">Create Account</h1> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> <mat-icon matPrefix>portrait</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput formControlName="email" type="email"> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput formControlName="password" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Confirm Password</mat-label> <input matInput formControlName="confirmedPassword" type="password"> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <div *ngIf="confirmedPassword?.invalid && (confirmedPassword?.dirty || confirmedPassword?.touched)"> <mat-error *ngIf="signupForm.hasError('differentPasswords')"> Your passwords do not match. </mat-error> </div> <div *ngIf="password?.invalid && (password?.dirty || password?.touched)"> <mat-error *ngIf="password?.hasError('minlength')"> Your password should be at least 8 characters. </mat-error> </div> <button mat-flat-button color="primary" [disabled]="!signupForm.valid">Sign Up</button> </form>

المكون سوف تتحول على النحو التالي.

لقطة شاشة لصفحة التسجيل
لقطة شاشة لصفحة التسجيل. (معاينة كبيرة)

مكون تسجيل الدخول

يقوم المستخدم المسجل بتسجيل الدخول إلى حسابه باستخدام هذا المكون. يجب إدخال البريد الإلكتروني وكلمة المرور. سيكون لحقول الإدخال المقابلة الخاصة بهم التحقق من الصحة الذي يجعلها مطلوبة.

على غرار SignupComponent ، يتم إخفاء أزرار عربة التسوق وتسجيل الدخول والخروج في الرأس. يتم تعيين رؤيتها باستخدام HeaderService أثناء تهيئة المكون.

لتسجيل الدخول ، يتم تمرير بيانات الاعتماد إلى خدمة AuthenticationService . إذا نجحت ، يتم تعيين حالة تسجيل الدخول للمستخدم باستخدام SessionService . ثم يتم توجيه المستخدم مرة أخرى إلى الصفحة التي كان عليها. في حالة عدم النجاح ، يتم عرض شريط الوجبات الخفيفة مع وجود خطأ وإعادة تعيين حقل كلمة المرور.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { loginForm = this.fb.group({ email: ['', Validators.required], password: ['', Validators.required] }); constructor( private authService: AuthenticationService, private session: SessionService, private snackBar: MatSnackBar, private fb: FormBuilder, private header: HeaderService, private location: Location ) { } ngOnInit() { this.header.setHeaderButtonsVisibility(false); } login() { const credentials = this.loginForm.value; this.authService.login( credentials.email, credentials.password ).subscribe( () => { this.session.setLoggedInStatus(true); this.location.back(); }, err => { this.snackBar.open( 'Login failed. Check your login credentials.', 'Close', { duration: 6000 }); this.loginForm.patchValue({ password: '' }); } ); } }

يوجد أدناه نموذج LoginComponent .

 <form [formGroup]="loginForm" (ngSubmit)="login()"> <h1 class="mat-display-3">Login</h1> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput type="email" formControlName="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-icon matPrefix>vpn_key</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="!loginForm.valid">Login</button> <p class="mat-h3">Not registered yet? <a routerLink="/signup">Create an account.</a></p> </form>

هنا لقطة من الصفحة.

لقطة شاشة لصفحة تسجيل الدخول
لقطة شاشة لصفحة تسجيل الدخول. (معاينة كبيرة)

وحدة العربة

تحتوي وحدة العربة على جميع الصفحات المتعلقة بعربة التسوق. يتضمن ذلك صفحة ملخص الطلب ، وصفحة رمز القسيمة وبطاقة الهدايا ، وصفحة سلة التسوق الفارغة. إنه منظم على النحو التالي.

 src/app/features/cart/ ├── cart.module.ts └── pages ├── codes │ ├── codes.component.css │ ├── codes.component.html │ └── codes.component.ts ├── empty │ ├── empty.component.css │ ├── empty.component.html │ └── empty.component.ts └── summary ├── summary.component.css ├── summary.component.html └── summary.component.ts

لإنشائه ، قم بتشغيل:

 ng gm features/cart ng gc features/cart/codes ng gc features/cart/empty ng gc features/cart/summary

هذا هو ملف الوحدة النمطية.

 @NgModule({ declarations: [SummaryComponent, CodesComponent, EmptyComponent], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'cart', component: SummaryComponent }, { path: 'codes', component: CodesComponent } ] }, { path: 'empty', component: EmptyComponent } ]), MatDividerModule, MatFormFieldModule, MatInputModule, MatMenuModule, ReactiveFormsModule, SharedModule ] }) export class CartModule { }

مكون الرموز

كما ذكرنا سابقًا ، يتم استخدام هذا المكون لإضافة أي قسيمة أو رموز بطاقة هدايا إلى أحد الطلبات. هذا يسمح للمستخدم بتطبيق الخصومات على إجمالي طلبيته قبل الشروع في الخروج.

سيكون هناك حقلا إدخال. واحد للقسائم والآخر لرموز بطاقات الهدايا.

تتم إضافة الرموز عن طريق تحديث الطلب. تقوم طريقة updateOrder الخاصة OrderService بتحديث الطلب بالرموز. بعد ذلك ، يتم إعادة تعيين كلا الحقلين ويتم إبلاغ المستخدم بنجاح العملية باستخدام شريط الوجبات الخفيفة. يظهر شريط الوجبات الخفيفة أيضًا عند حدوث خطأ. تستدعي كل من addCoupon و addGiftCard طريقة updateOrder .

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-codes', templateUrl: './codes.component.html', styleUrls: ['./codes.component.css'] }) export class CodesComponent { couponCode = new FormControl(''); giftCardCode = new FormControl(''); @ViewChild(FormControlDirective) codesDirective: FormControlDirective | undefined; constructor( private cart: CartService, private order: OrderService, private snackBar: MatSnackBar ) { } private updateOrder(order: Order, params: UpdateOrderParams[], codeType: string) { this.order.updateOrder(order, params) .subscribe( () => { this.snackBar.open(`Successfully added ${codeType} code.`, 'Close', { duration: 8000 }); this.couponCode.reset(); this.giftCardCode.reset(); this.codesDirective?.reset(); }, err => this.snackBar.open(`There was a problem adding your ${codeType} code.`, 'Close', { duration: 8000 }) ); } addCoupon() { this.updateOrder({ id: this.cart.orderId, couponCode: this.couponCode.value }, [UpdateOrderParams.couponCode], 'coupon'); } addGiftCard() { this.updateOrder({ id: this.cart.orderId, giftCardCode: this.giftCardCode.value }, [UpdateOrderParams.giftCardCode], 'gift card'); } }

النموذج موضح أدناه ويمكن العثور على التصميم الخاص به على هذا الرابط.

 <div> <app-title title="Redeem a code" subtitle="Enter a coupon code or gift card" [centerText]="true"></app-title> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Coupon Code</mat-label> <input matInput [formControl]="couponCode" required> <mat-icon matPrefix>card_giftcard</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="couponCode.invalid" (click)="addCoupon()">Redeem</button> </div> <div class="input-row"> <mat-form-field appearance="outline"> <mat-label>Gift Card Code</mat-label> <input matInput [formControl]="giftCardCode" required> <mat-icon matPrefix>redeem</mat-icon> </mat-form-field> <button class="redeem" mat-flat-button color="accent" [disabled]="giftCardCode.invalid" (click)="addGiftCard()">Redeem</button> </div> <button color="primary" mat-flat-button routerLink="/cart"> <mat-icon>shopping_cart</mat-icon> CONTINUE TO CART </button> </div>

هنا لقطة من الصفحة.

لقطة شاشة لصفحة الرموز
لقطة شاشة لصفحة الرموز. (معاينة كبيرة)

مكون فارغ

لا ينبغي أن يكون من الممكن إتمام الدفع بعربة فارغة. يجب أن يكون هناك حارس يمنع المستخدمين من الوصول إلى صفحات وحدة الدفع بعربات فارغة. تم بالفعل تغطية هذا كجزء من CoreModule . يقوم الحارس بإعادة توجيه الطلبات إلى صفحات الإعارة التي تحتوي على عربة فارغة إلى EmptyCartComponent .

إنه مكون بسيط للغاية يحتوي على بعض النص الذي يشير إلى المستخدم أن سلة التسوق الخاصة به فارغة. يحتوي أيضًا على زر يمكن للمستخدم النقر فوقه للانتقال إلى الصفحة الرئيسية لإضافة أشياء إلى سلة التسوق الخاصة به. لذلك سنستخدم SimplePageComponent لعرضها. ها هو النموذج.

 <app-simple-page title="Your cart is empty" subtitle="There is currently nothing in your cart. Head to the home page to add items." buttonText="GO TO HOME PAGE" icon="shopping_basket" [centerText]="true" route="/"> </app-simple-page>

هنا لقطة من الصفحة.

لقطة شاشة لصفحة عربة التسوق الفارغة
لقطة شاشة لصفحة عربة التسوق الفارغة. (معاينة كبيرة)

مكون الملخص

يلخص هذا المكون سلة التسوق / الطلب. يسرد جميع العناصر الموجودة في العربة وأسمائها وكمياتها وصورها. بالإضافة إلى ذلك ، فإنه يكسر تكلفة الطلب بما في ذلك الضرائب والشحن والخصومات. يجب أن يكون المستخدم قادرًا على عرض هذا وتحديد ما إذا كان راضيًا عن العناصر والتكلفة قبل الشروع في الخروج.

عند التهيئة ، يتم جلب الطلب وبنوده باستخدام OrderService . يجب أن يكون المستخدم قادرًا على تعديل البنود أو حتى إزالتها من الطلب. تتم إزالة العناصر عند استدعاء طريقة deleteLineItem . في ذلك ، يتلقى أسلوب deleteLineItem الخاص بـ LineItemService معرف عنصر السطر المراد حذفه. في حالة نجاح الحذف ، نقوم بتحديث عدد العناصر في سلة التسوق باستخدام CartService .

ثم يتم توجيه المستخدم إلى صفحة العميل حيث يبدأ عملية السحب. طريقة checkout تقوم بالتوجيه.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-summary', templateUrl: './summary.component.html', styleUrls: ['./summary.component.css'] }) export class SummaryComponent implements OnInit { order: Order = {}; summary: { name: string, amount: string | undefined, id: string }[] = []; constructor( private orders: OrderService, private lineItems: LineItemService, private cart: CartService, private snackBar: MatSnackBar, private router: Router ) { } ngOnInit() { this.orders.getOrder(this.cart.orderId, GetOrderParams.cart) .subscribe( order => this.processOrder(order), err => this.showOrderError('retrieving your cart') ); } private processOrder(order: Order) { this.order = order; this.summary = [ { name: 'Subtotal', amount: order.formattedSubtotalAmount, id: 'subtotal' }, { name: 'Discount', amount: order.formattedDiscountAmount, id: 'discount' }, { name: 'Taxes (included)', amount: order.formattedTotalTaxAmount, id: 'taxes' }, { name: 'Shipping', amount: order.formattedShippingAmount, id: 'shipping' }, { name: 'Gift Card', amount: order.formattedGiftCardAmount, id: 'gift-card' } ]; } private showOrderError(msg: string) { this.snackBar.open(`There was a problem ${msg}.`, 'Close', { duration: 8000 }); } checkout() { this.router.navigateByUrl('/customer'); } deleteLineItem(id: string) { this.lineItems.deleteLineItem(id) .pipe( mergeMap(() => this.orders.getOrder(this.cart.orderId, GetOrderParams.cart)) ).subscribe( order => { this.processOrder(order); this.cart.itemCount = order.skusCount || this.cart.itemCount; this.snackBar.open(`Item successfully removed from cart.`, 'Close', { duration: 8000 }) }, err => this.showOrderError('deleting your order') ); } }

يوجد أدناه القالب وتصميمه مرتبط هنا.

 <div class="container" *ngIf="order"> <h3>Order #{{order.number}} ({{order.skusCount}} items)</h3> <div class="line-item" *ngFor="let item of order.lineItems"> <div> <img *ngIf="item.imageUrl" class="image-xs" src="{{item.imageUrl}}" alt="product photo"> <div *ngIf="!item.imageUrl" class="image-xs no-image"></div> <div> <div>{{item.name}}</div> <div> {{item.formattedUnitAmount }} </div> </div> </div> <div> <app-item-quantity [quantity]="item.quantity || 0" [disabled]="true"></app-item-quantity> <div class="itemTotal"> {{item.formattedTotalAmount }} </div> <button mat-icon-button color="warn" (click)="deleteLineItem(item.id || '')"> <mat-icon>clear</mat-icon> </button> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem" *ngFor="let item of summary" [id]="item.id"> <h3 class="costLabel">{{item.name}}</h3> <p> {{item.amount }} </p> </div> </div> <mat-divider></mat-divider> <div class="costSummary"> <div class="costItem"> <h2>Total</h2> <h2> {{order.formattedTotalAmountWithTaxes}} </h2> </div> </div> <div> <button color="accent" mat-flat-button routerLink="/codes"> <mat-icon>redeem</mat-icon> ADD GIFT CARD/COUPON </button> <button color="primary" mat-flat-button (click)="checkout()"> <mat-icon>point_of_sale</mat-icon> CHECKOUT </button> </div> </div>

هنا لقطة من الصفحة.

لقطة شاشة لصفحة الملخص
لقطة شاشة لصفحة الملخص. (معاينة كبيرة)

وحدة الخروج

هذه الوحدة مسؤولة عن عملية الخروج. يتضمن تسجيل الخروج تقديم عنوان الفوترة والشحن ، والبريد الإلكتروني للعميل ، واختيار طريقة الشحن والدفع. الخطوة الأخيرة في هذه العملية هي تحديد وتأكيد الطلب. هيكل الوحدة على النحو التالي.

 src/app/features/checkout/ ├── components │ ├── address │ ├── address-list │ └── country-select └── pages ├── billing-address ├── cancel-payment ├── customer ├── payment ├── place-order ├── shipping-address └── shipping-methods

هذه الوحدة هي الأكبر حتى الآن وتحتوي على 3 مكونات و 7 صفحات. لتوليدها وتشغيل مكوناتها:

 ng gm features/checkout for comp in \ address address-list country-select; do \ ng gc "features/checkout/components/${comp}" \ ; done for page in \ billing-address cancel-payment customer \ payment place-order shipping-address \ shipping-methods; do \ ng gc "features/checkout/pages/${page}"; done

هذا هو ملف الوحدة النمطية.

 @NgModule({ declarations: [ CustomerComponent, AddressComponent, BillingAddressComponent, ShippingAddressComponent, ShippingMethodsComponent, PaymentComponent, PlaceOrderComponent, AddressListComponent, CountrySelectComponent, CancelPaymentComponent ], imports: [ RouterModule.forChild([ { path: '', canActivate: [EmptyCartGuard], children: [ { path: 'billing-address', component: BillingAddressComponent }, { path: 'cancel-payment', component: CancelPaymentComponent }, { path: 'customer', component: CustomerComponent }, { path: 'payment', component: PaymentComponent }, { path: 'place-order', component: PlaceOrderComponent }, { path: 'shipping-address', component: ShippingAddressComponent }, { path: 'shipping-methods', component: ShippingMethodsComponent } ] } ]), MatCardModule, MatCheckboxModule, MatDividerModule, MatInputModule, MatMenuModule, MatRadioModule, ReactiveFormsModule, SharedModule ] }) export class CheckoutModule { }

عناصر

البلد حدد المكون

يتيح هذا المكون للمستخدم تحديد بلد كجزء من العنوان. يتميز مكون تحديد المواد بمظهر مختلف تمامًا عند مقارنته بحقول الإدخال في نموذج العنوان. لذلك من أجل التوحيد ، يتم استخدام مكون قائمة المواد بدلاً من ذلك.

عند تهيئة المكون ، يتم جلب بيانات رمز البلد باستخدام CountryService . تحتفظ خاصية countries بالقيم التي تعرضها الخدمة. ستتم إضافة هذه القيم إلى القائمة الموجودة في النموذج.

يحتوي المكون على خاصية إخراج واحدة ، setCountryEvent . عند تحديد بلد ما ، يصدر هذا الحدث رمز alpha-2 الخاص بالبلد.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-country-select', templateUrl: './country-select.component.html', styleUrls: ['./country-select.component.css'] }) export class CountrySelectComponent implements OnInit { country: string = 'Country'; countries: Country[] = []; @Output() setCountryEvent = new EventEmitter<string>(); constructor(private countries: CountryService) { } ngOnInit() { this.countries.getCountries() .subscribe( countries => { this.countries = countries; } ); } setCountry(value: Country) { this.country = value.name || ''; this.setCountryEvent.emit(value.code); }}

يوجد أدناه قالبه والمرتبط هنا بتصميمه.

 <button mat-stroked-button [matMenuTriggerFor]="countryMenu"> {{country}} <mat-icon>expand_more</mat-icon> </button> <mat-menu #countryMenu="matMenu"> <button *ngFor="let cnt of countries" (click)="setCountry(cnt)" mat-menu-item>{{cnt.name}}</button> </mat-menu>

مكون العنوان

هذا شكل من أشكال التقاط العناوين. يتم استخدامه بواسطة كل من صفحات عناوين الشحن والفواتير. يجب أن يحتوي عنوان "الطبقة التجارية" الصالح على الاسم الأول والأخير ، وسطر العنوان ، والمدينة ، والرمز البريدي ، ورمز الولاية ، ورمز الدولة ، ورقم الهاتف.

ستقوم خدمة FormBuilder بإنشاء مجموعة النموذج. نظرًا لاستخدام هذا المكون بواسطة صفحات متعددة ، فإنه يحتوي على عدد من خصائص الإدخال والإخراج. تتضمن خصائص الإدخال نص الزر والعنوان المعروض ونص مربع الاختيار. ستكون خصائص الإخراج بمثابة بواعث للأحداث عند النقر فوق الزر لإنشاء العنوان وآخر عند تغيير قيمة مربع الاختيار.

عند النقر فوق الزر ، يتم استدعاء طريقة addAddress ويبعث حدث createAddress العنوان الكامل. وبالمثل ، عند تحديد خانة الاختيار ، فإن حدث isCheckboxChecked يصدر قيمة خانة الاختيار.

 @Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.css'] }) export class AddressComponent { @Input() buttonText: string = ''; @Input() showTitle?: boolean = false; @Output() createAddress = new EventEmitter<Address>(); @Input() checkboxText: string = ''; @Output() isCheckboxChecked = new EventEmitter<boolean>(); countryCode: string = ''; addressForm = this.fb.group({ firstName: [''], lastName: [''], line1: [''], city: [''], zipCode: [''], stateCode: [''], phone: [''] }); @ViewChild(FormGroupDirective) afDirective: FormGroupDirective | undefined; constructor(private fb: FormBuilder) { } setCountryCode(code: string) { this.countryCode = code; } addAddress() { this.createAddress.emit({ firstName: this.addressForm.get('firstName')?.value, lastName: this.addressForm.get('lastName')?.value, line1: this.addressForm.get('line1')?.value, city: this.addressForm.get('city')?.value, zipCode: this.addressForm.get('zipCode')?.value, stateCode: this.addressForm.get('stateCode')?.value || 'N/A', countryCode: this.countryCode, phone: this.addressForm.get('phone')?.value }); } setCheckboxValue(change: MatCheckboxChange) { if (this.isCheckboxChecked) { this.isCheckboxChecked.emit(change.checked); } } }

This is its template and its styling is linked here.

 <form [formGroup]="addressForm"> <p class="mat-headline" *ngIf="showTitle">Or add a new address</p> <div class="row"> <mat-form-field appearance="outline"> <mat-label>First Name</mat-label> <input matInput formControlName="firstName"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Last Name</mat-label> <input matInput formControlName="lastName"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Address</mat-label> <input matInput formControlName="line1"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>City</mat-label> <input matInput formControlName="city"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>State Code</mat-label> <input matInput formControlName="stateCode"> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Zip Code</mat-label> <input matInput formControlName="zipCode"> </mat-form-field> </div> <div class="row"> <mat-form-field appearance="outline"> <mat-label>Phone</mat-label> <input matInput formControlName="phone"> </mat-form-field> <app-country-select (setCountryEvent)="setCountryCode($event)"></app-country-select> </div> <mat-checkbox color="accent" (change)="setCheckboxValue($event)"> {{checkboxText}} </mat-checkbox> <button mat-flat-button color="primary" (click)="addAddress()"> {{buttonText}} </button> </form>

Address List Component

When a customer logs in, they can access their existing addresses. Instead of having them re-enter an address, they can pick from an address list. This is the purpose of this component. On initialization, all the customer's addresses are fetched using the CustomerAddressService if they are logged in. We will check their login status using the SessionService .

This component has a setAddressEvent output property. When an address is selected, setAddressEvent emits its id to the parent component.

@Component({ selector: 'app-address-list', templateUrl: './address-list.component.html', styleUrls: ['./address-list.component.css'] }) export class AddressListComponent implements OnInit { addresses: CustomerAddress[] = []; @Output() setAddressEvent = new EventEmitter<string>(); constructor( private session: SessionService, private customerAddresses: CustomerAddressService, private snackBar: MatSnackBar ) { } ngOnInit() { this.session.loggedInStatus .pipe( mergeMap( status => iif(() => status, this.customerAddresses.getCustomerAddresses()) )) .subscribe( addresses => { if (addresses.length) { this.addresses = addresses } }, err => this.snackBar.open('There was a problem getting your existing addresses.', 'Close', { duration: 8000 }) ); } setAddress(change: MatRadioChange) { this.setAddressEvent.emit(change.value); } }

هنا هو القالب الخاص به. يمكنك أن تجد أسلوبها هنا.

 <div> <p class="mat-headline">Pick an existing address</p> <mat-error *ngIf="!addresses.length">You have no existing addresses</mat-error> <mat-radio-group *ngIf="addresses.length" class="addresses" (change)="setAddress($event)"> <mat-card class="address" *ngFor="let address of addresses"> <mat-radio-button [value]="address.address?.id" color="primary"> <p>{{address.address?.firstName}} {{address.address?.lastName}},</p> <p>{{address.address?.line1}},</p> <p>{{address.address?.city}},</p> <p>{{address.address?.zipCode}},</p> <p>{{address.address?.stateCode}}, {{address.address?.countryCode}}</p> <p>{{address.address?.phone}}</p> </mat-radio-button> </mat-card> </mat-radio-group> </div>

الصفحات

مكون العميل

يحتاج الأمر إلى أن يكون مرتبطًا بعنوان بريد إلكتروني. هذا المكون هو نموذج يلتقط عنوان البريد الإلكتروني للعميل. عند تهيئة المكون ، يتم جلب عنوان البريد الإلكتروني للعميل الحالي إذا تم تسجيل دخوله. نحصل على العميل من CustomerService . إذا لم يرغبوا في تغيير عنوان بريدهم الإلكتروني ، فسيكون هذا البريد الإلكتروني هو القيمة الافتراضية.

إذا تم تغيير البريد الإلكتروني أو لم يتم تسجيل دخول العميل ، يتم تحديث الطلب بالبريد الإلكتروني المدخل. نستخدم OrderService لتحديث الطلب بعنوان البريد الإلكتروني الجديد. في حالة النجاح ، نقوم بتوجيه العميل إلى صفحة عنوان الفواتير.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-customer', templateUrl: './customer.component.html', styleUrls: ['./customer.component.css'] }) export class CustomerComponent implements OnInit { email = new FormControl('', [Validators.required, Validators.email]); constructor( private orders: OrderService, private customers: CustomerService, private cart: CartService, private router: Router, private snackBar: MatSnackBar ) { } ngOnInit() { this.customers.getCurrentCustomer() .subscribe( customer => this.email.setValue(customer.email) ); } addCustomerEmail() { this.orders.updateOrder( { id: this.cart.orderId, customerEmail: this.email.value }, [UpdateOrderParams.customerEmail]) .subscribe( () => this.router.navigateByUrl('/billing-address'), err => this.snackBar.open('There was a problem adding your email to the order.', 'Close', { duration: 8000 }) ); } }

هنا قالب المكون والمرتبط هنا هو التصميم الخاص به.

 <div> <app-title no="1" title="Customer" subtitle="Billing information and shipping address"></app-title> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput [formControl]="email" required> <mat-icon matPrefix>alternate_email</mat-icon> </mat-form-field> <button mat-flat-button color="primary" [disabled]="email.invalid" (click)="addCustomerEmail()"> PROCEED TO BILLING ADDRESS </button> </div>

هذه لقطة شاشة لصفحة العميل.

لقطة شاشة لصفحة العميل
لقطة شاشة لصفحة العميل. (معاينة كبيرة)

مكون عنوان الفواتير

يتيح مكون عنوان إرسال الفواتير للعميل إما إضافة عنوان فوترة جديد أو الاختيار من عناوينهم الحالية. يجب على المستخدمين الذين لم يقوموا بتسجيل الدخول إدخال عنوان جديد. أولئك الذين قاموا بتسجيل الدخول يحصلون على خيار للاختيار بين العناوين الجديدة أو الحالية.

تشير الخاصية showAddress إلى ما إذا كان يجب عرض العناوين الموجودة على المكون. يشير sameShippingAddressAsBilling إلى ما إذا كان عنوان الشحن يجب أن يكون هو نفسه عنوان إرسال الفواتير. عندما يختار العميل عنوانًا موجودًا ، يتم تعيين معرفه إلى selectedCustomerAddressId .

عند تهيئة المكون ، نستخدم SessionService للتحقق مما إذا كان المستخدم الحالي قد قام بتسجيل الدخول. إذا تم تسجيل دخوله ، فسنعرض عناوينهم الحالية إذا كان لديهم أي عناوين.

كما ذكرنا سابقًا ، إذا قام المستخدم بتسجيل الدخول ، فيمكنه اختيار عنوان موجود كعنوان إرسال الفواتير الخاص به. في طريقة updateBillingAddress ، إذا تم تسجيل دخولهم ، فسيتم نسخ العنوان الذي حددوه وتعيينه كعنوان فوترة الطلب. نقوم بذلك عن طريق تحديث الطلب باستخدام طريقة updateOrder الخاصة OrderService وتوفير معرف العنوان.

إذا لم يتم تسجيل دخولهم ، يجب على المستخدم تقديم عنوان. بمجرد تقديم العنوان ، يتم إنشاؤه باستخدام طريقة createAddress . في ذلك ، تأخذ AddressService المدخلات وتجعل العنوان الجديد. بعد ذلك ، يتم تحديث الطلب باستخدام معرف العنوان الذي تم إنشاؤه حديثًا. إذا كان هناك خطأ أو نجحت أي من العمليتين ، فإننا نعرض شريط الوجبات الخفيفة.

إذا تم تحديد نفس العنوان كعنوان شحن ، فسيتم توجيه المستخدم إلى صفحة طرق الشحن. إذا كانوا يرغبون في تقديم عنوان شحن بديل ، فسيتم توجيههم إلى صفحة عنوان الشحن.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-billing-address', templateUrl: './billing-address.component.html', styleUrls: ['./billing-address.component.css'] }) export class BillingAddressComponent implements OnInit { showAddresses: boolean = false; sameShippingAddressAsBilling: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateBillingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameShippingAddressAsBilling(change: boolean) { this.sameShippingAddressAsBilling = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, billingAddressId: address.id }, [UpdateOrderParams.billingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, billingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.billingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameShippingAddressAsBilling, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.shippingAddressSameAsBilling]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private navigateTo(path: string) { setTimeout(() => this.router.navigateByUrl(path), 4000); } private showSuccessSnackBar() { this.snackBar.open('Billing address successfully added. Redirecting...', 'Close', { duration: 3000 }); if (this.sameShippingAddressAsBilling) { this.navigateTo('/shipping-methods'); } else { this.navigateTo('/shipping-address'); } } }

ها هو النموذج. يشير هذا الارتباط إلى أسلوبه.

 <app-title no="2" title="Billing Address" subtitle="Address to bill charges to"></app-title> <app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list> <mat-divider *ngIf="showAddresses"></mat-divider> <app-address [showTitle]="showAddresses" buttonText="PROCEED TO NEXT STEP" checkboxText="Ship to the same address" (isCheckboxChecked)="setSameShippingAddressAsBilling($event)" (createAddress)="updateBillingAddress($event)"></app-address>

هذا هو الشكل الذي ستبدو عليه صفحة عنوان إرسال الفواتير.

لقطة شاشة لصفحة عنوان إرسال الفواتير
لقطة شاشة لصفحة عنوان إرسال الفواتير. (معاينة كبيرة)

مكون عنوان الشحن

يتصرف مكون عنوان الشحن إلى حد كبير مثل مكون عنوان إرسال الفواتير. ومع ذلك ، هناك نوعان من الاختلافات. أولاً ، النص المعروض في النموذج مختلف. الاختلافات الرئيسية الأخرى في كيفية تحديث الطلب باستخدام OrderService بمجرد إنشاء عنوان أو تحديده. الحقول التي يتم تحديث الطلب بها هي shippingAddressCloneId للعناوين المحددة وعنوان shippingAddress للعناوين الجديدة. إذا اختار المستخدم تغيير عنوان الفوترة ، ليكون هو نفسه عنوان الشحن ، فسيتم تحديث حقل billingAddressSameAsShipping .

بعد تحديد عنوان الشحن وتحديث الطلب ، يتم توجيه المستخدم إلى صفحة طرق الشحن.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-address', templateUrl: './shipping-address.component.html', styleUrls: ['./shipping-address.component.css'] }) export class ShippingAddressComponent implements OnInit { showAddresses: boolean = false; sameBillingAddressAsShipping: boolean = false; selectedCustomerAddressId: string = ''; constructor( private addresses: AddressService, private snackBar: MatSnackBar, private session: SessionService, private orders: OrderService, private cart: CartService, private router: Router, private customerAddresses: CustomerAddressService) { } ngOnInit() { this.session.loggedInStatus .subscribe( status => this.showAddresses = status ); } updateShippingAddress(address: Address) { if (this.showAddresses && this.selectedCustomerAddressId) { this.cloneAddress(); } else if (address.firstName && address.lastName && address.line1 && address.city && address.zipCode && address.stateCode && address.countryCode && address.phone) { this.createAddress(address); } else { this.snackBar.open('Check your address. Some fields are missing.', 'Close'); } } setCustomerAddress(customerAddressId: string) { this.selectedCustomerAddressId = customerAddressId; } setSameBillingAddressAsShipping(change: boolean) { this.sameBillingAddressAsShipping = change; } private createAddress(address: Address) { this.addresses.createAddress(address) .pipe( concatMap( address => { const update = this.updateOrderObservable({ id: this.cart.orderId, shippingAddressId: address.id }, [UpdateOrderParams.shippingAddress]); if (this.showAddresses) { return combineLatest([update, this.customerAddresses.createCustomerAddress(address.id || '', '')]); } else { return update; } })) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private cloneAddress() { this.updateOrderObservable({ id: this.cart.orderId, shippingAddressCloneId: this.selectedCustomerAddressId }, [UpdateOrderParams.shippingAddressClone]) .subscribe( () => this.showSuccessSnackBar(), err => this.showErrorSnackBar() ); } private updateOrderObservable(order: Order, updateParams: UpdateOrderParams[]): Observable<any> { return iif(() => this.sameBillingAddressAsShipping, concat([ this.orders.updateOrder(order, updateParams), this.orders.updateOrder(order, [UpdateOrderParams.billingAddressSameAsShipping]) ]), this.orders.updateOrder(order, updateParams) ); } private showErrorSnackBar() { this.snackBar.open('There was a problem creating your address.', 'Close', { duration: 8000 }); } private showSuccessSnackBar() { this.snackBar.open('Shipping address successfully added. Redirecting...', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/shipping-methods'), 4000); } }

هذا هو النموذج ويمكن العثور على التصميم الخاص به هنا.

 <app-title no="3" title="Shipping Address" subtitle="Address to ship package to"></app-title> <app-address-list *ngIf="showAddresses" (setAddressEvent)="setCustomerAddress($event)"></app-address-list> <mat-divider *ngIf="showAddresses"></mat-divider> <app-address [showTitle]="showAddresses" buttonText="PROCEED TO SHIPPING METHODS" checkboxText="Bill to the same address" (isCheckboxChecked)="setSameBillingAddressAsShipping($event)" (createAddress)="updateShippingAddress($event)"></app-address>

ستبدو صفحة عنوان الشحن هكذا.

لقطة شاشة لصفحة عنوان الشحن
لقطة شاشة لصفحة عنوان الشحن. (معاينة كبيرة)

مكون طرق الشحن

يعرض هذا المكون عدد الشحنات المطلوبة لاستيفاء الأمر وطرق الشحن المتاحة والتكاليف المرتبطة بها. يمكن للعميل بعد ذلك تحديد طريقة الشحن التي يفضلها لكل شحنة.

تحتوي خاصية shipments على جميع شحنات الطلب. نموذج shipmentsForm هو النموذج الذي سيتم من خلاله تحديد اختيارات طريقة الشحن.

عند تهيئة المكون ، يتم جلب الطلب وسيحتوي على كل من البنود والشحنات الخاصة به. في الوقت نفسه ، نحصل على مهل التسليم لطرق الشحن المختلفة. نحن نستخدم OrderService للحصول على الطلب و DeliveryLeadTimeService للمدد الزمنية. بمجرد إرجاع مجموعتي المعلومات ، يتم دمجها في مجموعة من الشحنات وتخصيصها لخاصية shipments . ستحتوي كل شحنة على عناصرها وطرق الشحن المتاحة والتكلفة المقابلة.

بعد أن يختار المستخدم طريقة شحن لكل شحنة ، يتم تحديث طريقة الشحن المحددة لكل شحنة في setShipmentMethods . إذا نجحت ، يتم توجيه المستخدم إلى صفحة المدفوعات.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-shipping-methods', templateUrl: './shipping-methods.component.html', styleUrls: ['./shipping-methods.component.css'] }) export class ShippingMethodsComponent implements OnInit { shipments: Shipment[] | undefined = []; shipmentsForm: FormGroup = this.fb.group({}); constructor( private orders: OrderService, private dlts: DeliveryLeadTimeService, private cart: CartService, private router: Router, private fb: FormBuilder, private shipments: ShipmentService, private snackBar: MatSnackBar ) { } ngOnInit() { combineLatest([ this.orders.getOrder(this.cart.orderId, GetOrderParams.shipments), this.dlts.getDeliveryLeadTimes() ]).subscribe( ([lineItems, deliveryLeadTimes]) => { let li: LineItem; let lt: DeliveryLeadTime[]; this.shipments = lineItems.shipments?.map((shipment) => { if (shipment.id) { this.shipmentsForm.addControl(shipment.id, new FormControl('', Validators.required)); } if (shipment.lineItems) { shipment.lineItems = shipment.lineItems.map(item => { li = this.findItem(lineItems, item.skuCode || ''); item.imageUrl = li.imageUrl; item.name = li.name; return item; }); } if (shipment.availableShippingMethods) { lt = this.findLocationLeadTime(deliveryLeadTimes, shipment); shipment.availableShippingMethods = shipment.availableShippingMethods?.map( method => { method.deliveryLeadTime = this.findMethodLeadTime(lt, method); return method; }); } return shipment; }); }, err => this.router.navigateByUrl('/error') ); } setShipmentMethods() { const shipmentsFormValue = this.shipmentsForm.value; combineLatest(Object.keys(shipmentsFormValue).map( key => this.shipments.updateShipment(key, shipmentsFormValue[key]) )).subscribe( () => { this.snackBar.open('Your shipments have been updated with a shipping method.', 'Close', { duration: 3000 }); setTimeout(() => this.router.navigateByUrl('/payment'), 4000); }, err => this.snackBar.open('There was a problem adding shipping methods to your shipments.', 'Close', { duration: 5000 }) ); } private findItem(lineItems: LineItem[], skuCode: string): LineItem { return lineItems.filter((item) => item.skuCode == skuCode)[0]; } private findLocationLeadTime(times: DeliveryLeadTime[], shipment: Shipment): DeliveryLeadTime[] { return times.filter((dlTime) => dlTime?.stockLocation?.id == shipment?.stockLocation?.id); } private findMethodLeadTime(times: DeliveryLeadTime[], method: ShippingMethod): DeliveryLeadTime { return times.filter((dlTime) => dlTime?.shippingMethod?.id == method?.id)[0]; } }

هذا هو النموذج ويمكنك العثور على التصميم على هذا الرابط.

 <form [formGroup]="shipmentsForm"> <app-title no="4" title="Shipping Methods" subtitle="How to ship your packages"></app-title> <div class="shipment-container" *ngFor="let shipment of shipments; let j = index; let isLast = last"> <h1>Shipment {{j+1}} of {{shipments?.length}}</h1> <div class="row" *ngFor="let item of shipment.lineItems"> <img class="image-xs" [src]="item.imageUrl" alt="product photo"> <div> <h4>{{item.name}}</h4> <p>{{item.skuCode}}</p> </div> <div> <p>Quantity: </p>{{item.quantity}} </div> </div> <mat-radio-group [formControlName]="shipment?.id || j"> <mat-radio-button *ngFor="let method of shipment.availableShippingMethods" [value]="method.id"> <div class="radio-button"> <p>{{method.name}}</p> <div> <p class="radio-label">Cost:</p> <p> {{method.formattedPriceAmount}}</p> </div> <div> <p class="radio-label">Timeline:</p> <p> Available in {{method.deliveryLeadTime?.minDays}}-{{method.deliveryLeadTime?.maxDays}} days</p> </div> </div> </mat-radio-button> </mat-radio-group> <mat-divider *ngIf="!isLast"></mat-divider> </div> <button mat-flat-button color="primary" [disabled]="shipmentsForm.invalid" (click)="setShipmentMethods()">PROCEED TO PAYMENT</button> </form>

هذه لقطة شاشة لصفحة طرق الشحن.

لقطة شاشة لصفحة طرق الشحن
لقطة شاشة لصفحة طرق الشحن. (معاينة كبيرة)

مكون المدفوعات

في هذا المكون ، ينقر المستخدم على زر الدفع إذا كان يرغب في متابعة الدفع مقابل طلبه باستخدام Paypal. إن approvalUrl هو رابط Paypal الذي يتم توجيه المستخدم إليه عند النقر فوق الزر.

أثناء التهيئة ، نحصل على الطلب مع مصدر الدفع المضمن باستخدام OrderService . إذا تم تعيين مصدر دفع ، نحصل على معرفه ونسترجع مدفوعات Paypal المقابلة من PaypalPaymentService . سيحتوي الدفع عن طريق Paypal على رابط الموافقة. إذا لم يتم تحديد مصدر الدفع ، نقوم بتحديث الطلب باستخدام Paypal كطريقة الدفع المفضلة. ننتقل بعد ذلك إلى إنشاء دفع Paypal جديد للطلب باستخدام PaypalPaymentService . من هنا ، يمكننا الحصول على عنوان url الخاص بالموافقة من الأمر الذي تم إنشاؤه حديثًا.

أخيرًا ، عندما ينقر المستخدم على الزر ، تتم إعادة توجيهه إلى Paypal حيث يمكنه الموافقة على الشراء.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css'] }) export class PaymentComponent implements OnInit { approvalUrl: string = ''; constructor( private orders: OrderService, private cart: CartService, private router: Router, private payments: PaypalPaymentService ) { } ngOnInit() { const orderId = this.cart.orderId; this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap((order: Order) => { const paymentSourceId = order.paymentSource?.id; const paymentMethod = order.availablePaymentMethods?.filter( (method) => method.paymentSourceType == 'paypal_payments' )[0]; return iif( () => paymentSourceId ? true : false, this.payments.getPaypalPayment(paymentSourceId || ''), this.orders.updateOrder({ id: orderId, paymentMethodId: paymentMethod?.id }, [UpdateOrderParams.paymentMethod]) .pipe(concatMap( order => this.payments.createPaypalPayment({ orderId: orderId, cancelUrl: `${environment.clientUrl}/cancel-payment`, returnUrl: `${environment.clientUrl}/place-order` }) )) ); })) .subscribe( paypalPayment => this.approvalUrl = paypalPayment?.approvalUrl || '', err => this.router.navigateByUrl('/error') ); } navigateToPaypal() { window.location.href = this.approvalUrl; } }

ها هو قالبها.

 <app-simple-page number="5" title="Payment" subtitle="Pay for your order" buttonText="PROCEED TO PAY WITH PAYPAL" icon="point_of_sale" (buttonEvent)="navigateToPaypal()" [buttonDisabled]="approvalUrl.length ? false : true"></app-simple-page>

هذا هو الشكل الذي ستبدو عليه صفحة المدفوعات.

لقطة شاشة لصفحة الدفع
لقطة شاشة لصفحة الدفع. (معاينة كبيرة)

إلغاء مكون الدفع

يتطلب Paypal صفحة إلغاء الدفع. يخدم هذا المكون هذا الغرض. هذا هو قالبها.

 <app-simple-page title="Payment cancelled" subtitle="Your Paypal payment has been cancelled" icon="money_off" buttonText="GO TO HOME" [centerText]="true" route="/"></app-simple-page>

هذه لقطة شاشة للصفحة.

لقطة شاشة لصفحة إلغاء الدفع
لقطة شاشة لصفحة إلغاء الدفع. (معاينة كبيرة)

مكوّن الطلب

هذه هي الخطوة الأخيرة في عملية السداد. هنا يؤكد المستخدم أنه يريد بالفعل تقديم الطلب وبدء معالجته. عندما يوافق المستخدم على دفع Paypal ، فهذه هي الصفحة التي يتم إعادة توجيهه إليها. يضيف Paypal معلمة استعلام معرف دافع إلى عنوان url. هذا هو معرف Paypal للمستخدم.

عند تهيئة المكون ، نحصل على معلمة استعلام payerId من عنوان url. ثم يتم استرداد الطلب باستخدام OrderService مع تضمين مصدر الدفع. يتم استخدام معرف مصدر الدفع المضمن لتحديث مدفوعات Paypal بمعرف الدافع ، باستخدام خدمة PaypalPayment . في حالة فشل أي منها ، تتم إعادة توجيه المستخدم إلى صفحة الخطأ. نستخدم خاصية " disableButton " لمنع المستخدم من تقديم الطلب حتى يتم تعيين معرف الدافع.

عندما ينقرون على زر الطلب ، يتم تحديث الطلب بحالة " placed ". بعد مسح العربة ، يتم عرض شريط وجبات خفيفة ناجح ، ويتم إعادة توجيه المستخدم إلى الصفحة الرئيسية.

 @UntilDestroy({ checkProperties: true }) @Component({ selector: 'app-place-order', templateUrl: './place-order.component.html', styleUrls: ['./place-order.component.css'] }) export class PlaceOrderComponent implements OnInit { disableButton = true; constructor( private route: ActivatedRoute, private router: Router, private payments: PaypalPaymentService, private orders: OrderService, private cart: CartService, private snackBar: MatSnackBar ) { } ngOnInit() { this.route.queryParams .pipe( concatMap(params => { const payerId = params['PayerID']; const orderId = this.cart.orderId; return iif( () => payerId.length > 0, this.orders.getOrder(orderId, GetOrderParams.paymentSource) .pipe( concatMap(order => { const paymentSourceId = order.paymentSource?.id || ''; return iif( () => paymentSourceId ? paymentSourceId.length > 0 : false, this.payments.updatePaypalPayment(paymentSourceId, payerId) ); }) ) ); })) .subscribe( () => this.disableButton = false, () => this.router.navigateByUrl('/error') ); } placeOrder() { this.disableButton = true; this.orders.updateOrder({ id: this.cart.orderId, place: true }, [UpdateOrderParams.place]) .subscribe( () => { this.snackBar.open('Your order has been successfully placed.', 'Close', { duration: 3000 }); this.cart.clearCart(); setTimeout(() => this.router.navigateByUrl('/'), 4000); }, () => { this.snackBar.open('There was a problem placing your order.', 'Close', { duration: 8000 }); this.disableButton = false; } ); } }

هذا هو النموذج والتصميم المرتبط به.

 <app-simple-page title="Finalize Order" subtitle="Complete your order" [number]="'6'" icon="shopping_bag" buttonText="PLACE YOUR ORDER" (buttonEvent)="placeOrder()" [buttonDisabled]="disableButton"></app-simple-page>

هنا لقطة من الصفحة.

لقطة شاشة لصفحة ترتيب الطلب
لقطة شاشة لصفحة ترتيب الطلب. (معاينة كبيرة)

وحدة التطبيق

يجب أن تحتوي جميع الطلبات المقدمة إلى Commerce Layer ، بخلاف المصادقة ، على رمز مميز. لذلك في اللحظة التي يتم فيها تهيئة التطبيق ، يتم جلب رمز مميز من المسار /oauth/token على الخادم ويتم تهيئة الجلسة. سنستخدم رمز APP_INITIALIZER لتقديم وظيفة تهيئة يتم من خلالها استرداد الرمز المميز. بالإضافة إلى ذلك ، سنستخدم الرمز المميز HTTP_INTERCEPTORS لتوفير OptionsInterceptor الذي أنشأناه سابقًا. بمجرد إضافة جميع الوحدات ، يجب أن يبدو ملف وحدة التطبيق كما يلي.

 @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, BrowserAnimationsModule, AuthModule, ProductsModule, CartModule, CheckoutModule, CoreModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: OptionsInterceptor, multi: true }, { provide: APP_INITIALIZER, useFactory: (http: HttpClient) => () => http.post<object>( `${environment.apiUrl}/oauth/token`, { 'grantType': 'client_credentials' }, { withCredentials: true }), multi: true, deps: [HttpClient] } ], bootstrap: [AppComponent] }) export class AppModule { }

مكون التطبيق

سنقوم بتعديل قالب مكون التطبيق وتصميمه الذي يمكنك العثور عليه هنا.

 <div> <app-header></app-header> <div> <router-outlet></router-outlet> </div> </div>

خاتمة

في هذه المقالة ، قمنا بتغطية كيف يمكنك إنشاء تطبيق Angular 11 للتجارة الإلكترونية باستخدام Commerce Layer و Paypal. لقد تطرقنا أيضًا إلى كيفية هيكلة التطبيق وكيف يمكنك التفاعل مع واجهة برمجة تطبيقات التجارة الإلكترونية.

على الرغم من أن هذا التطبيق يسمح للعميل بإجراء طلب كامل ، إلا أنه لم ينته بأي حال من الأحوال. هناك الكثير الذي يمكنك إضافته لتحسينه. لأحدها ، يمكنك اختيار تمكين تغييرات كمية العناصر في سلة التسوق ، وربط عناصر عربة التسوق بصفحات المنتج ، وتحسين مكونات العنوان ، وإضافة وسائل حماية إضافية لصفحات الخروج مثل صفحة طلب المكان ، وما إلى ذلك. هذه مجرد نقطة البداية.

إذا كنت ترغب في فهم المزيد حول عملية إصدار أمر من البداية إلى النهاية ، فيمكنك الاطلاع على أدلة طبقة التجارة وواجهة برمجة التطبيقات. يمكنك عرض الكود الخاص بهذا المشروع في هذا المستودع.