Files
vmall/apps/mall/plugins/api.ts
T
james e540b91491 feat(mall): live group-buying page with open or join intent
The group-buying page lists active activities, the group price against the
catalog price, and each open group with its paid-member count and expiry. A
shopper opens a group or picks one to join; either way the activity SKU goes
into the cart and the intent is carried to checkout, which submits it and hides
the coupon picker for that order. Payment and order detail show the snapshotted
group price.

The group-buying fixtures leave the page; the fixed-data adapter still serves
the domain as the rollback path.
2026-09-18 13:22:47 +00:00

133 lines
4.0 KiB
TypeScript

import { createApi } from "@vmall/shared";
import type { ApiClient } from "@vmall/shared";
import { createMockApi } from "~/mock/api";
/**
* Domains the live backend serves. Every other domain stays on the fixed-data
* adapter, so a domain can be migrated - or rolled back - by editing this list
* alone. See openspec/changes/replace-mock-api-wave-1/design.md.
*/
type LiveDomain =
| "auth"
| "account"
| "catalog"
| "currency"
| "content"
| "shops"
| "brands"
| "cart"
| "orders"
| "shipments"
| "invoices"
| "addresses"
| "coupons"
| "points"
| "flashSales"
| "groupBuying";
/**
* Explicit per-domain method picks rather than a string allowlist: indexing
* `ApiClient` by a union of method names would need an unsafe cast, and this
* keeps the compiler checking that every picked key exists.
*/
const LIVE_PICKS = {
auth: (a: ApiClient) => ({ register: a.register, login: a.login, me: a.me }),
account: (a: ApiClient) => ({ getAccountSummary: a.getAccountSummary }),
catalog: (a: ApiClient) => ({
listProducts: a.listProducts,
getProduct: a.getProduct,
listCategories: a.listCategories,
}),
currency: (a: ApiClient) => ({ listCurrencies: a.listCurrencies, convert: a.convert }),
content: (a: ApiClient) => ({ getHomeContent: a.getHomeContent }),
shops: (a: ApiClient) => ({ listShops: a.listShops, getShop: a.getShop }),
brands: (a: ApiClient) => ({ listBrands: a.listBrands }),
cart: (a: ApiClient) => ({
getCart: a.getCart,
addCartItem: a.addCartItem,
updateCartItem: a.updateCartItem,
removeCartItem: a.removeCartItem,
}),
orders: (a: ApiClient) => ({
checkout: a.checkout,
listMyOrders: a.listMyOrders,
getOrder: a.getOrder,
cancelOrder: a.cancelOrder,
payOrder: a.payOrder,
}),
shipments: (a: ApiClient) => ({
confirmDelivered: a.confirmDelivered,
listMyShipments: a.listMyShipments,
}),
invoices: (a: ApiClient) => ({
requestInvoice: a.requestInvoice,
listMyInvoices: a.listMyInvoices,
}),
addresses: (a: ApiClient) => ({
listMyAddresses: a.listMyAddresses,
createAddress: a.createAddress,
updateAddress: a.updateAddress,
deleteAddress: a.deleteAddress,
setDefaultAddress: a.setDefaultAddress,
}),
coupons: (a: ApiClient) => ({
listShopCouponTemplates: a.listShopCouponTemplates,
listMyCoupons: a.listMyCoupons,
claimCoupon: a.claimCoupon,
}),
points: (a: ApiClient) => ({
listPointsProducts: a.listPointsProducts,
listMyRedemptions: a.listMyRedemptions,
redeemPoints: a.redeemPoints,
}),
flashSales: (a: ApiClient) => ({ listFlashSales: a.listFlashSales }),
groupBuying: (a: ApiClient) => ({
listGroupBuyingActivities: a.listGroupBuyingActivities,
}),
} satisfies Record<LiveDomain, (a: ApiClient) => Partial<ApiClient>>;
const KNOWN_DOMAINS = Object.keys(LIVE_PICKS) as LiveDomain[];
/** Fallback when runtimeConfig supplies no list; keep in step with nuxt.config.ts. */
const DEFAULT_LIVE_DOMAINS: LiveDomain[] = [
"catalog",
"currency",
"content",
"shops",
"brands",
"auth",
"account",
"cart",
"orders",
"shipments",
"invoices",
"addresses",
"coupons",
"points",
"flashSales",
"groupBuying",
];
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
const mock = createMockApi();
const live = createApi({
baseUrl: config.public.apiBase as string,
getToken: () => (import.meta.client ? localStorage.getItem("vmall.token") : null),
});
const configured = config.public.liveDomains as string[] | undefined;
const liveDomains = (configured ?? DEFAULT_LIVE_DOMAINS).filter((domain): domain is LiveDomain =>
KNOWN_DOMAINS.includes(domain as LiveDomain),
);
// The fixed-data adapter is the base object, so an unmigrated domain cannot
// regress and a live domain can be rolled back by removing one entry.
let api: ApiClient = mock;
for (const domain of liveDomains) {
api = { ...api, ...LIVE_PICKS[domain](live) };
}
return { provide: { api } };
});