كيفية إنشاء موقع للتجارة الإلكترونية باستخدام Angular 11 وطبقة التجارة و Paypal
نشرت: 2022-03-10في الوقت الحاضر ، من الضروري أن يكون لديك وجود عبر الإنترنت عند إدارة الأعمال التجارية. يتم التسوق عبر الإنترنت أكثر بكثير مما كان عليه في السنوات السابقة. يسمح امتلاك متجر للتجارة الإلكترونية لأصحاب المتاجر بفتح مصادر أخرى للإيرادات لا يمكنهم الاستفادة منها بمجرد وجود متجر من الطوب وقذائف الهاون. ومع ذلك ، فإن أصحاب المتاجر الآخرين يديرون أعمالهم عبر الإنترنت بالكامل دون وجود مادي. هذا يجعل وجود متجر على الإنترنت أمرًا بالغ الأهمية.
تجعل مواقع مثل 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 .
لربط حسابك التجاري على Paypal بمؤسسة Commerce Layer ، انتقل إلى لوحة معلومات مؤسستك. هنا ستضيف بوابة دفع Paypal وطريقة دفع Paypal لأسواقك المختلفة. ضمن الإعدادات> المدفوعات ، حدد بوابات الدفع> Paypal وأضف معرف عميل Paypal وسرًا.
بعد إنشاء البوابة ، ستحتاج إلى إنشاء طريقة دفع Paypal لكل سوق تستهدفه لجعل Paypal متاحًا كخيار. ستقوم بذلك ضمن الإعدادات> المدفوعات> طرق الدفع> طريقة الدفع الجديدة .
ملاحظة حول المسارات المستخدمة
توفر طبقة التجارة مسارًا للمصادقة ومجموعة أخرى مختلفة من المسارات لواجهة برمجة التطبيقات الخاصة بهم. يتبادل مسار المصادقة /oauth/token
بيانات الاعتماد لرمز مميز. هذا الرمز مطلوب للوصول إلى واجهة برمجة التطبيقات الخاصة بهم. تأخذ بقية مسارات API النمط /api/:resource
.
يغطي نطاق هذه المقالة فقط الجزء الأمامي من هذا التطبيق. اخترت تخزين جانب الخادم المميز ، واستخدام الجلسات لتتبع الملكية ، وتقديم ملفات تعريف ارتباط http فقط مع معرف جلسة إلى العميل. لن يتم تناول هذا هنا لأنه خارج نطاق هذه المقالة. ومع ذلك ، تظل المسارات كما هي وتتوافق تمامًا مع Commerce Layer API. على الرغم من وجود مسارين مخصصين غير متاحين من Commerce Layer API التي سنستخدمها. هذه تتعامل بشكل أساسي مع إدارة الجلسة. سوف أشير إلى هذه عندما نصل إليهم ووصف كيف يمكنك تحقيق نتيجة مماثلة.
هناك تناقض آخر قد تلاحظه وهو أن هيئات الطلب تختلف عما تتطلبه Commerce Layer API. نظرًا لأن الطلبات يتم تمريرها إلى خادم آخر لتتم تعبئتها برمز مميز ، فقد صنعت الهيئات بشكل مختلف. كان هذا لتسهيل إرسال الطلبات. في حالة وجود أي تناقضات في هيئات الطلب ، سيتم الإشارة إلى ذلك في الخدمات.
نظرًا لأن هذا خارج النطاق ، فسيتعين عليك تحديد كيفية تخزين الرموز المميزة بأمان. ستحتاج أيضًا إلى تعديل طفيف لهيئات الطلب لتطابق تمامًا ما تتطلبه واجهة برمجة تطبيقات Commerce Layer. عندما يكون هناك تناقض ، سأقوم بالربط بمرجع API وأدلة توضح بالتفصيل كيفية هيكلة الجسم بشكل صحيح.
هيكل التطبيق
لتنظيم التطبيق ، سنقسمه إلى أربعة أجزاء رئيسية. يتم تقديم وصف أفضل لما تفعله كل وحدة ضمن الأقسام المقابلة لها:
- الوحدة الأساسية ،
- وحدة البيانات ،
- الوحدة المشتركة ،
- وحدات الميزة.
ستجمع وحدات الميزات الصفحات والمكونات ذات الصلة معًا. سيكون هناك أربع وحدات ميزات:
- وحدة المصادقة ،
- وحدة المنتج ،
- وحدة العربة ،
- وحدة الخروج.
عندما نصل إلى كل وحدة ، سأشرح الغرض منها وأقسم محتوياتها.
يوجد أدناه شجرة لمجلد 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.
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>
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. لقد تطرقنا أيضًا إلى كيفية هيكلة التطبيق وكيف يمكنك التفاعل مع واجهة برمجة تطبيقات التجارة الإلكترونية.
على الرغم من أن هذا التطبيق يسمح للعميل بإجراء طلب كامل ، إلا أنه لم ينته بأي حال من الأحوال. هناك الكثير الذي يمكنك إضافته لتحسينه. لأحدها ، يمكنك اختيار تمكين تغييرات كمية العناصر في سلة التسوق ، وربط عناصر عربة التسوق بصفحات المنتج ، وتحسين مكونات العنوان ، وإضافة وسائل حماية إضافية لصفحات الخروج مثل صفحة طلب المكان ، وما إلى ذلك. هذه مجرد نقطة البداية.
إذا كنت ترغب في فهم المزيد حول عملية إصدار أمر من البداية إلى النهاية ، فيمكنك الاطلاع على أدلة طبقة التجارة وواجهة برمجة التطبيقات. يمكنك عرض الكود الخاص بهذا المشروع في هذا المستودع.