วิธีสร้างเว็บไซต์อีคอมเมิร์ซด้วย Angular 11, Commerce Layer และ Paypal

เผยแพร่แล้ว: 2022-03-10
สรุปโดยย่อ ↬ การมีร้านอีคอมเมิร์ซเป็นสิ่งสำคัญสำหรับเจ้าของร้านค้า เนื่องจากลูกค้าหันมาซื้อของออนไลน์มากขึ้นเรื่อยๆ ในบทช่วยสอนนี้ เราจะพูดถึงวิธีสร้างไซต์อีคอมเมิร์ซด้วย Angular 11 ไซต์นี้จะใช้ Commerce Layer เป็น API อีคอมเมิร์ซแบบไม่มีส่วนหัว และใช้ Paypal ในการประมวลผลการชำระเงิน

ปัจจุบันนี้จำเป็นต้องมีสถานะออนไลน์เมื่อดำเนินธุรกิจ มีการซื้อของออนไลน์มากกว่าปีที่แล้ว การมีร้านค้าอีคอมเมิร์ซช่วยให้เจ้าของร้านสามารถเปิดแหล่งรายได้อื่นๆ ที่พวกเขาไม่สามารถใช้ประโยชน์จากร้านค้าที่มีหน้าร้านจริงได้ อย่างไรก็ตาม เจ้าของร้านค้ารายอื่นๆ ทำธุรกิจออนไลน์โดยไม่ต้องมีตัวตนอยู่จริง ทำให้การมีร้านค้าออนไลน์มีความสำคัญ

ไซต์เช่น Etsy, Shopify และ Amazon ทำให้ง่ายต่อการตั้งค่าร้านค้าอย่างรวดเร็วโดยไม่ต้องกังวลเกี่ยวกับการพัฒนาไซต์ อย่างไรก็ตาม อาจมีบางกรณีที่เจ้าของร้านอาจต้องการประสบการณ์ที่เป็นส่วนตัวหรืออาจประหยัดค่าใช้จ่ายในการเป็นเจ้าของร้านค้าบนแพลตฟอร์มเหล่านี้บางส่วน

แพลตฟอร์ม e-commerce API ที่ไม่มีหัวให้แบ็กเอนด์ที่จัดเก็บไซต์สามารถเชื่อมต่อได้ พวกเขาจัดการกระบวนการและข้อมูลทั้งหมดที่เกี่ยวข้องกับร้านค้า เช่น ลูกค้า คำสั่งซื้อ การจัดส่ง การชำระเงิน และอื่นๆ ทั้งหมดที่จำเป็นคือส่วนหน้าเพื่อโต้ตอบกับข้อมูลนี้ สิ่งนี้ทำให้เจ้าของมีความยืดหยุ่นมากเมื่อต้องตัดสินใจว่าลูกค้าจะได้พบกับร้านค้าออนไลน์ของพวกเขาอย่างไรและพวกเขาจะเลือกดำเนินการอย่างไร

ในบทความนี้ เราจะพูดถึงวิธีสร้างร้านค้าอีคอมเมิร์ซโดยใช้ Angular 11 เราจะใช้ Commerce Layer เป็น API อีคอมเมิร์ซแบบไม่มีส่วนหัว แม้ว่าอาจมีวิธีมากมายในการประมวลผลการชำระเงิน แต่เราจะแสดงวิธีใช้ Paypal เพียงวิธีเดียว

  • ดูซอร์สโค้ดบน GitHub →

ข้อกำหนดเบื้องต้น

ก่อนสร้างแอป คุณต้องติดตั้ง Angular CLI เราจะใช้มันเพื่อเริ่มต้นและนั่งร้านแอป หากคุณยังไม่ได้ติดตั้ง คุณสามารถติดตั้งผ่าน npm

 npm install -g @angular/cli

คุณจะต้องมีบัญชีนักพัฒนา Commerce Layer ด้วย เมื่อใช้บัญชีนักพัฒนาซอฟต์แวร์ คุณจะต้องสร้างองค์กรทดสอบและเริ่มต้นข้อมูลด้วยข้อมูลการทดสอบ Seeding ช่วยให้พัฒนาแอปก่อนได้ง่ายขึ้นโดยไม่ต้องกังวลว่าต้องใช้ข้อมูลใด คุณสามารถสร้างบัญชีได้ที่ลิงค์นี้และองค์กรที่นี่

แดชบอร์ดองค์กรบัญชีนักพัฒนา Commerce Layer
แดชบอร์ดองค์กรบัญชีนักพัฒนา Commerce Layer ที่คุณเพิ่มองค์กรของคุณ (ตัวอย่างขนาดใหญ่)
แบบฟอร์มการสร้างองค์กร Commerce Layer
เลือกช่อง Seed with test data เมื่อสร้างองค์กรใหม่ (ตัวอย่างขนาดใหญ่)

สุดท้าย คุณจะต้องมีบัญชี Paypal Sandbox การมีบัญชีประเภทนี้จะทำให้เราสามารถทดสอบการทำธุรกรรมระหว่างธุรกิจและผู้ใช้โดยไม่ต้องเสี่ยงกับเงินจริง คุณสามารถสร้างได้ที่นี่ บัญชีแซนด์บ็อกซ์มีธุรกิจทดสอบและทดสอบบัญชีส่วนตัวที่สร้างไว้แล้ว

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

Commerce Layer และการกำหนดค่า Paypal

หากต้องการชำระเงินด้วย Paypal Sandbox บน Commerce Layer คุณจะต้องตั้งค่าคีย์ API ไปที่ภาพรวมบัญชีของบัญชีนักพัฒนา Paypal ของคุณ เลือกบัญชีธุรกิจและภายใต้แท็บข้อมูลประจำตัว API ของรายละเอียดบัญชี คุณจะพบ แอปพลิเคชันเริ่มต้น ภายใต้ REST Apps

แท็บข้อมูลรับรอง API บนป๊อปอัปรายละเอียดบัญชีธุรกิจ Paypal Sandbox
จะหาแอป REST เริ่มต้นได้ที่ไหนในป๊อปอัปรายละเอียดบัญชีธุรกิจ Paypal (ตัวอย่างขนาดใหญ่)
ภาพรวมแอปพลิเคชันเริ่มต้นในการตั้งค่าบัญชีธุรกิจ Paypal Sandbox
ภาพรวมแอปพลิเคชันเริ่มต้นในการตั้งค่าบัญชีธุรกิจ Paypal Sandbox ซึ่งคุณสามารถรับรหัสไคลเอ็นต์ REST API และข้อมูลลับได้ (ตัวอย่างขนาดใหญ่)

หากต้องการเชื่อมโยงบัญชีธุรกิจ Paypal กับองค์กร Commerce Layer ให้ไปที่แดชบอร์ดขององค์กร ที่นี่ คุณจะเพิ่มเกตเวย์การชำระเงิน Paypal และวิธีการชำระเงิน Paypal สำหรับตลาดต่างๆ ของคุณ ภายใต้ การตั้งค่า > การชำระเงิน เลือก เกตเวย์การชำระเงิน > Paypal และเพิ่มรหัสลูกค้า Paypal และข้อมูลลับ

แดชบอร์ดเกตเวย์การชำระเงินใหม่บน Commerce Layer
ที่ไหนบนแดชบอร์ด Commerce Layer เพื่อสร้างเกตเวย์การชำระเงิน Paypal (ตัวอย่างขนาดใหญ่)

หลังจากสร้างเกตเวย์แล้ว คุณจะต้องสร้างวิธีการชำระเงิน Paypal สำหรับแต่ละตลาดที่คุณกำหนดเป้าหมายเพื่อให้ Paypal พร้อมใช้งานเป็นตัวเลือก คุณต้องดำเนินการนี้ใน การตั้งค่า > การชำระเงิน > วิธีการชำระเงิน > วิธีการชำระเงินใหม่

แดชบอร์ดวิธีการชำระเงินบน Commerce Layer
ที่ไหนบนแดชบอร์ด Commerce Layer เพื่อสร้างวิธีการชำระเงิน Paypal (ตัวอย่างขนาดใหญ่)

หมายเหตุเกี่ยวกับเส้นทางที่ใช้

Commerce Layer จัดเตรียมเส้นทางสำหรับการรับรองความถูกต้องและชุดเส้นทางอื่นสำหรับ API เส้นทางการรับรองความถูกต้อง /oauth/token ของพวกเขาจะแลกเปลี่ยนข้อมูลประจำตัวสำหรับโทเค็น ต้องใช้โทเค็นนี้เพื่อเข้าถึง API เส้นทาง API ที่เหลือใช้รูปแบบ /api/:resource

ขอบเขตของบทความนี้ครอบคลุมเฉพาะส่วนหน้าของแอปนี้ ฉันเลือกที่จะเก็บโทเค็นฝั่งเซิร์ฟเวอร์ ใช้เซสชันเพื่อติดตามความเป็นเจ้าของ และให้คุกกี้ http เท่านั้นพร้อมรหัสเซสชันให้กับลูกค้า สิ่งนี้จะไม่ครอบคลุมในที่นี้ เนื่องจากอยู่นอกขอบเขตของบทความนี้ อย่างไรก็ตาม เส้นทางยังคงเหมือนเดิมและสอดคล้องกับ Commerce Layer API ทุกประการ แม้ว่าจะมีเส้นทางที่กำหนดเองบางเส้นทางที่ไม่มีให้บริการจาก Commerce Layer API ที่เราจะใช้ สิ่งเหล่านี้ส่วนใหญ่เกี่ยวข้องกับการจัดการเซสชัน ฉันจะชี้ให้เห็นสิ่งเหล่านี้เมื่อเราไปถึงพวกเขา และอธิบายว่าคุณจะบรรลุผลลัพธ์ที่คล้ายคลึงกันได้อย่างไร

ความไม่สอดคล้องกันอีกประการหนึ่งที่คุณอาจสังเกตเห็นคือเนื้อหาคำขอแตกต่างจากที่ Commerce Layer API ต้องการ เนื่องจากคำขอถูกส่งไปยังเซิร์ฟเวอร์อื่นเพื่อรับโทเค็น ฉันจึงจัดโครงสร้างเนื้อหาให้แตกต่างออกไป เพื่อให้ง่ายต่อการส่งคำขอ เมื่อใดก็ตามที่มีความไม่สอดคล้องกันในหน่วยคำขอ สิ่งเหล่านี้จะถูกชี้ให้เห็นในบริการ

เนื่องจากสิ่งนี้อยู่นอกขอบเขต คุณจะต้องตัดสินใจว่าจะจัดเก็บโทเค็นอย่างปลอดภัยอย่างไร นอกจากนี้ คุณจะต้องแก้ไขเนื้อหาคำขอเล็กน้อยเพื่อให้ตรงกับความต้องการของ Commerce Layer API เมื่อมีความไม่สอดคล้องกัน ฉันจะลิงก์ไปยังข้อมูลอ้างอิง API และคำแนะนำโดยละเอียดเกี่ยวกับวิธีจัดโครงสร้างเนื้อหาอย่างถูกต้อง

โครงสร้างแอป

ในการจัดระเบียบแอพ เราจะแบ่งมันออกเป็นสี่ส่วนหลัก คำอธิบายที่ดีขึ้นเกี่ยวกับสิ่งที่แต่ละโมดูลทำอยู่ภายใต้ส่วนที่เกี่ยวข้อง:

  1. โมดูลหลัก,
  2. โมดูลข้อมูล
  3. โมดูลที่ใช้ร่วมกัน
  4. โมดูลคุณลักษณะ

โมดูลคุณลักษณะจะจัดกลุ่มเพจและส่วนประกอบที่เกี่ยวข้องกัน จะมีสี่โมดูลคุณลักษณะ:

  1. โมดูลตรวจสอบสิทธิ์,
  2. โมดูลผลิตภัณฑ์
  3. โมดูลรถเข็น,
  4. โมดูลการชำระเงิน

เมื่อเราเข้าสู่แต่ละโมดูล ฉันจะอธิบายว่าจุดประสงค์คืออะไรและแยกย่อยเนื้อหา

ด้านล่างเป็นแผนผังของโฟลเดอร์ src/app และตำแหน่งที่แต่ละโมดูลตั้งอยู่

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

การสร้างแอพและเพิ่มการพึ่งพา

เราจะเริ่มต้นด้วยการสร้างแอป องค์กรของเราจะเรียกว่า The LIme Brand และจะมีข้อมูลการทดสอบที่จัดทำโดย Commerce Layer แล้ว

 ng new lime-app

เราต้องการการอ้างอิงสองสามอย่าง ส่วนใหญ่เป็นวัสดุเชิงมุมและจนกว่าจะทำลาย วัสดุเชิงมุมจะจัดเตรียมส่วนประกอบและสไตล์ จนกว่า Destroy จะยกเลิกสมาชิกที่สังเกตได้โดยอัตโนมัติเมื่อส่วนประกอบถูกทำลาย ในการติดตั้งให้รัน:

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

ทรัพย์สิน

เมื่อเพิ่มที่อยู่ใน Commerce Layer ต้องใช้รหัสประเทศอัลฟ่า-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; }

ส่วนประกอบชื่อเรื่อง

องค์ประกอบนี้เพิ่มเป็นสองเท่าของชื่อ stepper เช่นเดียวกับชื่อธรรมดาในหน้าที่เรียบง่ายบางหน้า แม้ว่า Angular Material จะมีส่วนประกอบแบบ stepper แต่ก็ไม่เหมาะที่สุดสำหรับกระบวนการเช็คเอาต์ที่ค่อนข้างยาว ไม่ตอบสนองบนจอแสดงผลขนาดเล็ก และต้องใช้เวลามากขึ้นในการติดตั้ง ชื่อที่ง่ายกว่า แต่สามารถนำมาใช้ใหม่เป็นตัวบ่งชี้การก้าวและมีประโยชน์ในหลาย ๆ หน้า

 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>

สามารถดูสไตล์ได้ที่นี่

ท่อ

Word Wrap Pipe

ชื่อผลิตภัณฑ์บางรายการและข้อมูลประเภทอื่นๆ ที่แสดงบนเว็บไซต์นั้นยาวมาก ในบางกรณี การนำประโยคยาวๆ เหล่านี้มารวมไว้ในส่วนประกอบที่เป็นวัสดุเป็นสิ่งที่ท้าทาย เราจะใช้ไพพ์นี้ในการตัดประโยคให้เหลือความยาวที่กำหนด และเพิ่มจุดไข่ปลาที่ส่วนท้ายของผลลัพธ์

ในการสร้างให้รัน:

 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); } }

บริการจัดเก็บในพื้นที่

เราจะใช้พื้นที่จัดเก็บในพื้นที่เพื่อติดตามจำนวนสินค้าในรถเข็น นอกจากนี้ยังมีประโยชน์ในการจัดเก็บรหัสของคำสั่งซื้อที่นี่ คำสั่งซื้อสอดคล้องกับตะกร้าสินค้าบน Commerce Layer

ในการสร้างบริการจัดเก็บในเครื่องให้รัน:

 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

โมเดล

โมเดลต่างๆ เป็นตัวกำหนดว่าข้อมูลที่เราใช้จาก API มีโครงสร้างอย่างไร เราจะมีการประกาศอินเทอร์เฟซ 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 การชำระเงินผ่าน 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)); } }

บริการรถเข็น

รถเข็นมีหน้าที่รักษาปริมาณของสินค้าที่เพิ่มและรหัสคำสั่งซื้อ การเรียก API เพื่อรับจำนวนรายการในคำสั่งซื้อทุกครั้งที่มีการสร้างรายการโฆษณาใหม่อาจมีราคาแพง แต่เราสามารถใช้ที่เก็บข้อมูลในเครื่องเพื่อรักษาจำนวนบนไคลเอนต์ได้ ซึ่งช่วยลดความจำเป็นในการเรียกคำสั่งซื้อที่ไม่จำเป็นทุกครั้งที่มีการเพิ่มสินค้าลงในรถเข็น

เรายังใช้บริการนี้เพื่อจัดเก็บรหัสคำสั่งซื้อ ตะกร้าสินค้าสอดคล้องกับคำสั่งซื้อบน 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 รหัสประเทศจะต้องเป็นรหัสอัลฟ่า 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 บน Commerce Layer ดังนั้น คุณจะต้องหาวิธีรับลูกค้าที่เข้าสู่ระบบอยู่ในปัจจุบัน

 @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 โดยระบุรหัส เนื้อหาโพสต์ควรมีโครงสร้างคล้ายกับตัวอย่างนี้เมื่อสร้างการชำระเงิน 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 รับสินค้าจากร้านค้า หากมีการเรียกข้อมูลผลิตภัณฑ์หลายรายการ สามารถแบ่งหน้าและกำหนดขนาดหน้าได้ ควรกำหนดขนาดหน้าและหมายเลขหน้าเป็นพารามิเตอร์การค้นหา เช่นเดียวกับในตัวอย่างนี้ หากคุณกำลังส่งคำขอโดยตรงไปยัง API ผลิตภัณฑ์เดียวสามารถเรียกค้นรหัสได้

 @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 ช่วยให้คุณได้รับโทเค็นของลูกค้าและลูกค้า โทเค็นเหล่านี้ใช้เพื่อเข้าถึงเส้นทางที่เหลือของ API โทเค็นของลูกค้าจะถูกส่งคืนเมื่อผู้ใช้แลกเปลี่ยนอีเมลและรหัสผ่านและมีสิทธิ์ในขอบเขตที่กว้างขึ้น โทเค็นไคลเอ็นต์ออกโดยไม่จำเป็นต้องใช้ข้อมูลประจำตัวและมีสิทธิ์ที่แคบกว่า

getClientSession ได้รับโทเค็นไคลเอ็นต์ login ได้รับโทเค็นของลูกค้า ทั้งสองวิธียังสร้างเซสชัน เนื้อความของคำขอโทเค็นของลูกค้าควรมีลักษณะเช่นนี้ และของคำขอโทเค็นของลูกค้าเป็นแบบนี้

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

Session Service

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

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

Header Service

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

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

ส่วนประกอบ

Error Component

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

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

This is what the component will look like.

Screenshot of error page
Screenshot of error page. (ตัวอย่างขนาดใหญ่)

Not Found Component

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

 <app-simple-page title="404: Page not found" buttonText="GO TO HOME" icon="search" subtitle="The requested page could not be found" [centerText]="true" route="/"></app-simple-page>
Screenshot of 404 page
Screenshot of 404 page. (ตัวอย่างขนาดใหญ่)

Header Component

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

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

มีวิธี logout ที่ทำลายเซสชันของผู้ใช้และกำหนดโทเค็นไคลเอ็นต์ให้พวกเขา มีการกำหนดโทเค็นไคลเอ็นต์เนื่องจากเซสชันที่ดูแลโทเค็นของลูกค้าจะถูกทำลาย และยังคงต้องใช้โทเค็นสำหรับคำขอ API แต่ละรายการ สแน็คบาร์วัสดุจะสื่อสารกับผู้ใช้ว่าเซสชันของพวกเขาถูกทำลายสำเร็จหรือไม่

เราใช้ตัว @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'); } }

เครื่องสกัดกั้น

ตัวเลือก Interceptor

ตัวสกัดกั้นนี้จะสกัดกั้นคำขอ 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>

หน้าจะเป็นแบบนี้

สกรีนช็อตของหน้ารายการสินค้า
ภาพหน้าจอของหน้ารายการสินค้า (ตัวอย่างขนาดใหญ่)

ส่วนประกอบผลิตภัณฑ์

เมื่อสินค้าถูกเลือกจากหน้ารายการสินค้า ส่วนประกอบนี้จะแสดงรายละเอียด ซึ่งรวมถึงชื่อเต็ม ราคา และคำอธิบายของผลิตภัณฑ์ นอกจากนี้ยังมีปุ่มสำหรับเพิ่มสินค้าลงในตะกร้าสินค้าอีกด้วย

ในการเริ่มต้น เราได้รับรหัสของผลิตภัณฑ์จากพารามิเตอร์เส้นทาง โดยใช้ ID เราดึงผลิตภัณฑ์จาก 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 { }

รหัสส่วนประกอบ

ตามที่กล่าวไว้ก่อนหน้านี้ องค์ประกอบนี้ใช้เพื่อเพิ่มรหัสคูปองหรือบัตรของขวัญในคำสั่งซื้อ สิ่งนี้ทำให้ผู้ใช้สามารถใช้ส่วนลดกับยอดรวมของการสั่งซื้อก่อนที่จะดำเนินการชำระเงิน

จะมีช่องใส่ของ 2 ช่อง หนึ่งสำหรับคูปองและอีกอันสำหรับรหัสบัตรของขวัญ

รหัสจะถูกเพิ่มโดยการอัปเดตคำสั่งซื้อ วิธี 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 ได้รับ id ของรายการโฆษณาที่จะลบ หากการลบสำเร็จ เราจะอัปเดตจำนวนสินค้าในรถเข็นโดยใช้ 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 เมื่อเลือกประเทศ เหตุการณ์นี้จะปล่อยรหัสอัลฟ่า-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>

ส่วนประกอบที่อยู่

นี่คือแบบฟอร์มสำหรับเก็บที่อยู่ ใช้โดยทั้งหน้าที่อยู่สำหรับจัดส่งและที่อยู่สำหรับเรียกเก็บเงิน ที่อยู่ Commerce Layer ที่ถูกต้องควรมีชื่อและนามสกุล บรรทัดที่อยู่ เมือง รหัสไปรษณีย์ รหัสรัฐ รหัสประเทศ และหมายเลขโทรศัพท์

บริการ 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 ระบุว่าที่อยู่สำหรับจัดส่งควรเหมือนกับที่อยู่สำหรับการเรียกเก็บเงินที่ตั้งค่าไว้หรือไม่ เมื่อลูกค้าเลือกที่อยู่ที่มีอยู่ ID ของลูกค้าจะถูกกำหนดให้กับ 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 จะมี URL การอนุมัติ หากไม่มีการตั้งค่าแหล่งการชำระเงิน เราจะอัปเดตคำสั่งซื้อด้วย 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 เรายังได้พูดถึงวิธีจัดโครงสร้างแอปและวิธีที่คุณสามารถเชื่อมต่อกับ API อีคอมเมิร์ซได้

แม้ว่าแอพนี้จะอนุญาตให้ลูกค้าทำการสั่งซื้อได้อย่างสมบูรณ์ แต่ก็ไม่ได้ทำเสร็จด้วยวิธีการใดๆ มีอะไรอีกมากมายที่คุณสามารถเพิ่มเพื่อปรับปรุงได้ อย่างแรก คุณอาจเลือกที่จะเปิดใช้งานการเปลี่ยนแปลงปริมาณสินค้าในรถเข็น เชื่อมโยงรายการในรถเข็นไปยังหน้าผลิตภัณฑ์ เพิ่มประสิทธิภาพองค์ประกอบที่อยู่ เพิ่มยามเพิ่มเติมสำหรับหน้าชำระเงิน เช่น หน้าสั่งซื้อ และอื่นๆ นี่เป็นเพียงจุดเริ่มต้น

หากคุณต้องการเข้าใจเพิ่มเติมเกี่ยวกับขั้นตอนการสั่งซื้อตั้งแต่ต้นจนจบ คุณสามารถดู คู่มือ Commerce Layer และ API คุณสามารถดูรหัสสำหรับโครงการนี้ได้ที่ที่เก็บนี้