| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import { DropdownDirective } from '../../shared/dropdown.directive';
- import { CommonModule } from '@angular/common';
- import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
- import { FormsModule } from '@angular/forms';
- import { ActivatedRoute, Router, RouterLink } from '@angular/router';
- import { ChevronRight, FileText, LucideAngularModule, MapPin, PackageCheck, Store } from 'lucide-angular';
- import { finalize, forkJoin } from 'rxjs';
- import { ApiError, ApiService } from '../../core/api.service';
- import { AppUser } from '../../core/models';
- import { PlatformService } from '../../core/platform.service';
- import { SessionService } from '../../core/session.service';
- type Item = Record<string, unknown>;
- interface CheckoutAddress {
- ok: boolean;
- empty: boolean;
- userName: string;
- telNumber: string;
- provinceName: string;
- cityName: string;
- countyName: string;
- detailInfo: string;
- postalCode: string;
- }
- interface CheckoutInvoice {
- ok: boolean;
- aud: number;
- name: string;
- mobile: string;
- idnum: string;
- detail: string;
- address: string;
- bankname: string;
- bankusr: string;
- email: string;
- }
- @Component({
- selector: 'app-checkout-page',
- standalone: true,
- imports: [DropdownDirective, CommonModule, FormsModule, RouterLink, LucideAngularModule],
- templateUrl: './checkout-page.component.html',
- styleUrl: './checkout-page.component.scss',
- changeDetection: ChangeDetectionStrategy.OnPush,
- })
- export class CheckoutPageComponent implements OnInit {
- readonly carts = signal<Item[]>([]);
- readonly addresses = signal<Item[]>([]);
- readonly coupons = signal<Item[]>([]);
- readonly selectedAddress = signal<CheckoutAddress | null>(null);
- readonly selectedCoupon = signal<Item | null>(null);
- readonly pickup = signal(false);
- readonly invoiceEnabled = signal(false);
- readonly invoiceType = signal(0);
- readonly icons = { ChevronRight, FileText, MapPin, PackageCheck, Store };
- readonly subtotal = computed(() => this.carts().reduce((sum, item) => sum + this.cartTotal(item), 0));
- readonly availableCoupons = computed(() => this.coupons().filter((item) => {
- const amount = this.subtotal();
- return amount > Number(item['MinAmount'] ?? 0) && amount < Number(item['MaxAmount'] ?? Number.MAX_SAFE_INTEGER);
- }));
- readonly discount = computed(() => Number(this.selectedCoupon()?.['Amount'] ?? 0));
- readonly total = computed(() => Math.max(0, this.subtotal() - this.discount()));
- invoice: CheckoutInvoice = this.emptyInvoice();
- private readonly route = inject(ActivatedRoute);
- private readonly router = inject(Router);
- private readonly api = inject(ApiService);
- private readonly session = inject(SessionService);
- private readonly platform = inject(PlatformService);
- ngOnInit(): void {
- const uid = this.session.user()?.userId;
- const ids = this.route.snapshot.queryParamMap.get('cart') || '0';
- this.platform.loading.set(true);
- forkJoin({
- cart: this.api.get<unknown>('cart_list', { ids, uid }),
- addresses: this.api.get<unknown>('receaddr_list', { uid }),
- coupons: this.api.get<unknown>('coupon_usrgot_list', { uid, fast: 'idle' }),
- member: this.api.get<AppUser>('user_get', { uid }),
- }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
- next: ({ cart, addresses, coupons, member }) => {
- const rows = this.findList(cart.result, 'CartDT');
- this.carts.set(rows);
- this.addresses.set(this.findList(addresses.result));
- this.coupons.set(this.findList(coupons.result));
- this.session.patchUser({ ...member.result, ...this.asObject(member.addon) });
- if (!rows.length) this.platform.toast('购物车中没有可结算商品', 'error');
- },
- error: (error: ApiError) => this.platform.toast(`结算信息加载失败:${error.message}`, 'error'),
- });
- }
- selectAddress(event: Event): void {
- const id = (event.target as HTMLSelectElement).value;
- const item = this.addresses().find((address) => String(address['ID'] ?? address['id']) === id);
- if (!item) { this.selectedAddress.set(null); return; }
- const [provinceName = '', cityName = '', countyName = ''] = String(item['Provinces'] ?? '').split(' ');
- this.selectedAddress.set({
- ok: true,
- empty: false,
- userName: String(item['ReceivName'] ?? ''),
- telNumber: String(item['MobileNum'] ?? ''),
- provinceName,
- cityName,
- countyName,
- detailInfo: String(item['Street'] ?? ''),
- postalCode: String(item['Zipcode'] ?? ''),
- });
- }
- selectCoupon(event: Event): void {
- const id = (event.target as HTMLSelectElement).value;
- this.selectedCoupon.set(this.availableCoupons().find((item) => String(item['ID'] ?? item['id']) === id) ?? null);
- }
- toggleInvoice(enabled: boolean): void {
- this.invoiceEnabled.set(enabled);
- this.invoice.ok = enabled;
- }
- changeInvoiceType(type: number): void {
- this.invoiceType.set(type);
- this.invoice = { ...this.emptyInvoice(), ok: true, aud: type };
- }
- async settle(): Promise<void> {
- if (!this.pickup() && !this.selectedAddress()) { this.platform.toast('请选择收货地址或到店自提', 'error'); return; }
- if (!this.carts().length) { this.platform.toast('购物车中没有商品', 'error'); return; }
- if (!this.validateInvoice()) return;
- const user = this.session.user();
- const balance = Number(user?.silverCoin ?? user?.['SilverCoin'] ?? 0);
- if (balance < this.total()) {
- this.platform.toast('学习豆余额不足,请先充值', 'error');
- void this.router.navigate(['/pages/member/coin'], { queryParams: { charge: 1 } });
- return;
- }
- if (!(await this.platform.confirm(`即将消耗 ${this.total().toFixed(2)} 学习豆,确认支付?`))) return;
- const address = this.pickup() ? this.pickupAddress(user) : this.selectedAddress();
- this.platform.loading.set(true);
- this.api.post<unknown>('payment_cart', {
- cartIds: this.carts().map((item) => item['id'] ?? item['ID']).join(','),
- uid: user?.userId,
- rece: JSON.stringify(address),
- fareDT: JSON.stringify([{ storeId: '1', total: '0.00' }]),
- use_sicon: 1,
- invoice: this.invoiceEnabled() ? JSON.stringify({ ...this.invoice, ok: true, aud: this.invoiceType() }) : '',
- couponid: this.selectedCoupon()?.['ID'] ?? this.selectedCoupon()?.['id'] ?? null,
- }).subscribe({
- next: (payment) => {
- const order = this.parseOrder(payment.addon);
- this.api.post('payment_success', { uid: user?.userId, orderIds: order['orderIds'], cartIds: order['cartIds'] })
- .pipe(finalize(() => this.platform.loading.set(false)))
- .subscribe({
- next: () => {
- this.platform.toast('支付成功', 'success');
- this.refreshMember();
- void this.router.navigate(['/pages/shop/order'], { queryParams: { id: order['orderIds'] } });
- },
- error: (error: ApiError) => this.platform.toast(`支付已完成,但订单状态更新失败:${error.message}`, 'error'),
- });
- },
- error: (error: ApiError) => { this.platform.loading.set(false); this.platform.toast(`订单支付失败:${error.message}`, 'error'); },
- });
- }
- itemName(item: Item): string { return String(item['proname'] ?? item['Proname'] ?? item['ProName'] ?? item['Title'] ?? '商品'); }
- itemImage(item: Item): string { return this.api.assetUrl(item['proimg'] ?? item['Thumbnails'] ?? item['Pic']); }
- itemQuantity(item: Item): number { return Number(item['pronum'] ?? item['Pronum'] ?? 1); }
- itemPrice(item: Item): number { return this.cartTotal(item) / Math.max(1, this.itemQuantity(item)); }
- private cartTotal(item: Item): number {
- const quantity = this.itemQuantity(item);
- const user = this.session.user();
- if (Number(item['Nodeid'] ?? item['NodeID']) === 19) {
- if (Number(user?.groupId) === 2) return 199 * quantity;
- if (Number(user?.groupId) === 3) return 179 * quantity;
- if (Number(user?.groupId) === 4) return 159 * quantity;
- if (Number(user?.['vip'] ?? 0) !== 0) return Math.floor(Number(item['AllMoney'] ?? 0) * 700) / 1000;
- }
- return Number(item['AllMoney'] ?? Number(item['price'] ?? item['LinPrice'] ?? 0) * quantity);
- }
- private validateInvoice(): boolean {
- if (!this.invoiceEnabled()) return true;
- const form = this.invoice;
- form.name = form.name.trim();
- form.idnum = form.idnum.trim();
- form.email = form.email.trim();
- form.mobile = form.mobile.trim();
- if (!form.name) { this.platform.toast(this.invoiceType() === 0 ? '请输入姓名' : '请输入企业名称', 'error'); return false; }
- if (!form.idnum) { this.platform.toast(this.invoiceType() === 0 ? '请输入身份证号码' : '请输入统一信用代码', 'error'); return false; }
- if (this.invoiceType() === 0 && !/^\d{17}[\dXx]$/.test(form.idnum)) { this.platform.toast('请输入正确的身份证号码', 'error'); return false; }
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { this.platform.toast('请输入正确的联系邮箱', 'error'); return false; }
- if (this.invoiceType() === 1 && !/^1\d{10}$/.test(form.mobile)) { this.platform.toast('请输入正确的企业注册电话', 'error'); return false; }
- return true;
- }
- private pickupAddress(user: AppUser | null): CheckoutAddress {
- return { ok: false, empty: true, userName: `${user?.honeyName ?? ''} [${user?.userName ?? ''}]`, telNumber: '', provinceName: '', cityName: '', countyName: '', detailInfo: '会员到店自提', postalCode: '' };
- }
- private emptyInvoice(): CheckoutInvoice {
- return { ok: false, aud: 0, name: '', mobile: '', idnum: '', detail: '信息技术服务费', address: '', bankname: '', bankusr: '', email: '' };
- }
- private refreshMember(): void {
- const uid = this.session.user()?.userId;
- if (!uid) return;
- this.api.get<AppUser>('user_get', { uid }).subscribe({ next: ({ result, addon }) => this.session.patchUser({ ...result, ...this.asObject(addon) }), error: () => undefined });
- }
- private findList(value: unknown, preferred?: string): Item[] {
- if (Array.isArray(value)) return value as Item[];
- if (value && typeof value === 'object') {
- const object = value as Item;
- if (preferred && Array.isArray(object[preferred])) return object[preferred] as Item[];
- for (const nested of Object.values(object)) if (Array.isArray(nested)) return nested as Item[];
- }
- return [];
- }
- private asObject(value: unknown): Item { return value && typeof value === 'object' && !Array.isArray(value) ? value as Item : {}; }
- private parseOrder(value: unknown): Item {
- if (typeof value === 'string') { try { return JSON.parse(value) as Item; } catch { return {}; } }
- return this.asObject(value);
- }
- }
|