checkout-page.component.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { DropdownDirective } from '../../shared/dropdown.directive';
  2. import { CommonModule } from '@angular/common';
  3. import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
  4. import { FormsModule } from '@angular/forms';
  5. import { ActivatedRoute, Router, RouterLink } from '@angular/router';
  6. import { ChevronRight, FileText, LucideAngularModule, MapPin, PackageCheck, Store } from 'lucide-angular';
  7. import { finalize, forkJoin } from 'rxjs';
  8. import { ApiError, ApiService } from '../../core/api.service';
  9. import { AppUser } from '../../core/models';
  10. import { PlatformService } from '../../core/platform.service';
  11. import { SessionService } from '../../core/session.service';
  12. type Item = Record<string, unknown>;
  13. interface CheckoutAddress {
  14. ok: boolean;
  15. empty: boolean;
  16. userName: string;
  17. telNumber: string;
  18. provinceName: string;
  19. cityName: string;
  20. countyName: string;
  21. detailInfo: string;
  22. postalCode: string;
  23. }
  24. interface CheckoutInvoice {
  25. ok: boolean;
  26. aud: number;
  27. name: string;
  28. mobile: string;
  29. idnum: string;
  30. detail: string;
  31. address: string;
  32. bankname: string;
  33. bankusr: string;
  34. email: string;
  35. }
  36. @Component({
  37. selector: 'app-checkout-page',
  38. standalone: true,
  39. imports: [DropdownDirective, CommonModule, FormsModule, RouterLink, LucideAngularModule],
  40. templateUrl: './checkout-page.component.html',
  41. styleUrl: './checkout-page.component.scss',
  42. changeDetection: ChangeDetectionStrategy.OnPush,
  43. })
  44. export class CheckoutPageComponent implements OnInit {
  45. readonly carts = signal<Item[]>([]);
  46. readonly addresses = signal<Item[]>([]);
  47. readonly coupons = signal<Item[]>([]);
  48. readonly selectedAddress = signal<CheckoutAddress | null>(null);
  49. readonly selectedCoupon = signal<Item | null>(null);
  50. readonly pickup = signal(false);
  51. readonly invoiceEnabled = signal(false);
  52. readonly invoiceType = signal(0);
  53. readonly icons = { ChevronRight, FileText, MapPin, PackageCheck, Store };
  54. readonly subtotal = computed(() => this.carts().reduce((sum, item) => sum + this.cartTotal(item), 0));
  55. readonly availableCoupons = computed(() => this.coupons().filter((item) => {
  56. const amount = this.subtotal();
  57. return amount > Number(item['MinAmount'] ?? 0) && amount < Number(item['MaxAmount'] ?? Number.MAX_SAFE_INTEGER);
  58. }));
  59. readonly discount = computed(() => Number(this.selectedCoupon()?.['Amount'] ?? 0));
  60. readonly total = computed(() => Math.max(0, this.subtotal() - this.discount()));
  61. invoice: CheckoutInvoice = this.emptyInvoice();
  62. private readonly route = inject(ActivatedRoute);
  63. private readonly router = inject(Router);
  64. private readonly api = inject(ApiService);
  65. private readonly session = inject(SessionService);
  66. private readonly platform = inject(PlatformService);
  67. ngOnInit(): void {
  68. const uid = this.session.user()?.userId;
  69. const ids = this.route.snapshot.queryParamMap.get('cart') || '0';
  70. this.platform.loading.set(true);
  71. forkJoin({
  72. cart: this.api.get<unknown>('cart_list', { ids, uid }),
  73. addresses: this.api.get<unknown>('receaddr_list', { uid }),
  74. coupons: this.api.get<unknown>('coupon_usrgot_list', { uid, fast: 'idle' }),
  75. member: this.api.get<AppUser>('user_get', { uid }),
  76. }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
  77. next: ({ cart, addresses, coupons, member }) => {
  78. const rows = this.findList(cart.result, 'CartDT');
  79. this.carts.set(rows);
  80. this.addresses.set(this.findList(addresses.result));
  81. this.coupons.set(this.findList(coupons.result));
  82. this.session.patchUser({ ...member.result, ...this.asObject(member.addon) });
  83. if (!rows.length) this.platform.toast('购物车中没有可结算商品', 'error');
  84. },
  85. error: (error: ApiError) => this.platform.toast(`结算信息加载失败:${error.message}`, 'error'),
  86. });
  87. }
  88. selectAddress(event: Event): void {
  89. const id = (event.target as HTMLSelectElement).value;
  90. const item = this.addresses().find((address) => String(address['ID'] ?? address['id']) === id);
  91. if (!item) { this.selectedAddress.set(null); return; }
  92. const [provinceName = '', cityName = '', countyName = ''] = String(item['Provinces'] ?? '').split(' ');
  93. this.selectedAddress.set({
  94. ok: true,
  95. empty: false,
  96. userName: String(item['ReceivName'] ?? ''),
  97. telNumber: String(item['MobileNum'] ?? ''),
  98. provinceName,
  99. cityName,
  100. countyName,
  101. detailInfo: String(item['Street'] ?? ''),
  102. postalCode: String(item['Zipcode'] ?? ''),
  103. });
  104. }
  105. selectCoupon(event: Event): void {
  106. const id = (event.target as HTMLSelectElement).value;
  107. this.selectedCoupon.set(this.availableCoupons().find((item) => String(item['ID'] ?? item['id']) === id) ?? null);
  108. }
  109. toggleInvoice(enabled: boolean): void {
  110. this.invoiceEnabled.set(enabled);
  111. this.invoice.ok = enabled;
  112. }
  113. changeInvoiceType(type: number): void {
  114. this.invoiceType.set(type);
  115. this.invoice = { ...this.emptyInvoice(), ok: true, aud: type };
  116. }
  117. async settle(): Promise<void> {
  118. if (!this.pickup() && !this.selectedAddress()) { this.platform.toast('请选择收货地址或到店自提', 'error'); return; }
  119. if (!this.carts().length) { this.platform.toast('购物车中没有商品', 'error'); return; }
  120. if (!this.validateInvoice()) return;
  121. const user = this.session.user();
  122. const balance = Number(user?.silverCoin ?? user?.['SilverCoin'] ?? 0);
  123. if (balance < this.total()) {
  124. this.platform.toast('学习豆余额不足,请先充值', 'error');
  125. void this.router.navigate(['/pages/member/coin'], { queryParams: { charge: 1 } });
  126. return;
  127. }
  128. if (!(await this.platform.confirm(`即将消耗 ${this.total().toFixed(2)} 学习豆,确认支付?`))) return;
  129. const address = this.pickup() ? this.pickupAddress(user) : this.selectedAddress();
  130. this.platform.loading.set(true);
  131. this.api.post<unknown>('payment_cart', {
  132. cartIds: this.carts().map((item) => item['id'] ?? item['ID']).join(','),
  133. uid: user?.userId,
  134. rece: JSON.stringify(address),
  135. fareDT: JSON.stringify([{ storeId: '1', total: '0.00' }]),
  136. use_sicon: 1,
  137. invoice: this.invoiceEnabled() ? JSON.stringify({ ...this.invoice, ok: true, aud: this.invoiceType() }) : '',
  138. couponid: this.selectedCoupon()?.['ID'] ?? this.selectedCoupon()?.['id'] ?? null,
  139. }).subscribe({
  140. next: (payment) => {
  141. const order = this.parseOrder(payment.addon);
  142. this.api.post('payment_success', { uid: user?.userId, orderIds: order['orderIds'], cartIds: order['cartIds'] })
  143. .pipe(finalize(() => this.platform.loading.set(false)))
  144. .subscribe({
  145. next: () => {
  146. this.platform.toast('支付成功', 'success');
  147. this.refreshMember();
  148. void this.router.navigate(['/pages/shop/order'], { queryParams: { id: order['orderIds'] } });
  149. },
  150. error: (error: ApiError) => this.platform.toast(`支付已完成,但订单状态更新失败:${error.message}`, 'error'),
  151. });
  152. },
  153. error: (error: ApiError) => { this.platform.loading.set(false); this.platform.toast(`订单支付失败:${error.message}`, 'error'); },
  154. });
  155. }
  156. itemName(item: Item): string { return String(item['proname'] ?? item['Proname'] ?? item['ProName'] ?? item['Title'] ?? '商品'); }
  157. itemImage(item: Item): string { return this.api.assetUrl(item['proimg'] ?? item['Thumbnails'] ?? item['Pic']); }
  158. itemQuantity(item: Item): number { return Number(item['pronum'] ?? item['Pronum'] ?? 1); }
  159. itemPrice(item: Item): number { return this.cartTotal(item) / Math.max(1, this.itemQuantity(item)); }
  160. private cartTotal(item: Item): number {
  161. const quantity = this.itemQuantity(item);
  162. const user = this.session.user();
  163. if (Number(item['Nodeid'] ?? item['NodeID']) === 19) {
  164. if (Number(user?.groupId) === 2) return 199 * quantity;
  165. if (Number(user?.groupId) === 3) return 179 * quantity;
  166. if (Number(user?.groupId) === 4) return 159 * quantity;
  167. if (Number(user?.['vip'] ?? 0) !== 0) return Math.floor(Number(item['AllMoney'] ?? 0) * 700) / 1000;
  168. }
  169. return Number(item['AllMoney'] ?? Number(item['price'] ?? item['LinPrice'] ?? 0) * quantity);
  170. }
  171. private validateInvoice(): boolean {
  172. if (!this.invoiceEnabled()) return true;
  173. const form = this.invoice;
  174. form.name = form.name.trim();
  175. form.idnum = form.idnum.trim();
  176. form.email = form.email.trim();
  177. form.mobile = form.mobile.trim();
  178. if (!form.name) { this.platform.toast(this.invoiceType() === 0 ? '请输入姓名' : '请输入企业名称', 'error'); return false; }
  179. if (!form.idnum) { this.platform.toast(this.invoiceType() === 0 ? '请输入身份证号码' : '请输入统一信用代码', 'error'); return false; }
  180. if (this.invoiceType() === 0 && !/^\d{17}[\dXx]$/.test(form.idnum)) { this.platform.toast('请输入正确的身份证号码', 'error'); return false; }
  181. if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { this.platform.toast('请输入正确的联系邮箱', 'error'); return false; }
  182. if (this.invoiceType() === 1 && !/^1\d{10}$/.test(form.mobile)) { this.platform.toast('请输入正确的企业注册电话', 'error'); return false; }
  183. return true;
  184. }
  185. private pickupAddress(user: AppUser | null): CheckoutAddress {
  186. return { ok: false, empty: true, userName: `${user?.honeyName ?? ''} [${user?.userName ?? ''}]`, telNumber: '', provinceName: '', cityName: '', countyName: '', detailInfo: '会员到店自提', postalCode: '' };
  187. }
  188. private emptyInvoice(): CheckoutInvoice {
  189. return { ok: false, aud: 0, name: '', mobile: '', idnum: '', detail: '信息技术服务费', address: '', bankname: '', bankusr: '', email: '' };
  190. }
  191. private refreshMember(): void {
  192. const uid = this.session.user()?.userId;
  193. if (!uid) return;
  194. this.api.get<AppUser>('user_get', { uid }).subscribe({ next: ({ result, addon }) => this.session.patchUser({ ...result, ...this.asObject(addon) }), error: () => undefined });
  195. }
  196. private findList(value: unknown, preferred?: string): Item[] {
  197. if (Array.isArray(value)) return value as Item[];
  198. if (value && typeof value === 'object') {
  199. const object = value as Item;
  200. if (preferred && Array.isArray(object[preferred])) return object[preferred] as Item[];
  201. for (const nested of Object.values(object)) if (Array.isArray(nested)) return nested as Item[];
  202. }
  203. return [];
  204. }
  205. private asObject(value: unknown): Item { return value && typeof value === 'object' && !Array.isArray(value) ? value as Item : {}; }
  206. private parseOrder(value: unknown): Item {
  207. if (typeof value === 'string') { try { return JSON.parse(value) as Item; } catch { return {}; } }
  208. return this.asObject(value);
  209. }
  210. }