feat(mall): serve catalog and currency from the live API
Wave 1 of replacing the fixed-data mock adapter. The mall now selects its API adapter per domain, with catalog and currency served live while auth, cart, orders, shipments and invoices stay on fixed data. Backend: - seed the 6 x 2 x 2 category tree as reference data (migration 0005). The API exposes no category write route, so this cannot come from the seed script - filter public product listing by the category subtree with a recursive CTE, matching the mock's existing behaviour instead of exact-match - add sort=price with order=asc|desc, validated by hand so an unsupported value returns the project's ApiError 400 shape rather than axum's own rejection Mall: - replace the all-or-nothing mockApi boolean with a liveDomains list composed through a typed per-domain pick map - source home floors, the category menu, search and product detail from the catalog API; banners, promos, quick links, store card and comment/coupon content stay local display-only content - drop the brand facet and the sales/comments sorts: no backend model backs them - fix salesOf/commentCountOf, which parsed digits out of the product id and so rendered "NaN sold" for live UUID ids; they now hash the id Seed: 24 products across 4 shops, idempotent on re-run. Note: the mall defaults to a live catalog, so pnpm dev:mall now expects the API to be running; set NUXT_PUBLIC_LIVE_DOMAINS to an empty array for all-mock work. OpenSpec change: openspec/changes/replace-mock-api-wave-1
This commit is contained in:
+15
-54
@@ -19,11 +19,6 @@ const L = (en: string, zh: string): LocalizedText => ({ en, zh });
|
||||
|
||||
// ---------- mall-local mock types ----------
|
||||
|
||||
export interface MockBrand {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MockStore {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -55,14 +50,6 @@ export interface MockPromo {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface HomeFloor {
|
||||
categoryId: string;
|
||||
name: LocalizedText;
|
||||
advImage: string;
|
||||
advUrl: string;
|
||||
products: Product[];
|
||||
}
|
||||
|
||||
export interface MockComment {
|
||||
id: string;
|
||||
productId: string;
|
||||
@@ -343,32 +330,10 @@ export function categorySubtreeIds(rootId: string): Set<string> {
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function topCategories(): Category[] {
|
||||
return MOCK_CATEGORIES.filter((c) => c.parent_id === null).sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
export function childCategories(parentId: string): Category[] {
|
||||
return MOCK_CATEGORIES.filter((c) => c.parent_id === parentId).sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
// ---------- brands ----------
|
||||
|
||||
export const MOCK_BRANDS: MockBrand[] = [
|
||||
{ id: "b1", name: "Aurora" },
|
||||
{ id: "b2", name: "Nordwind" },
|
||||
{ id: "b3", name: "Hexon" },
|
||||
{ id: "b4", name: "Mikado" },
|
||||
{ id: "b5", name: "Solace" },
|
||||
{ id: "b6", name: "Terra" },
|
||||
];
|
||||
|
||||
const PRODUCT_BRAND: Record<string, string> = {};
|
||||
|
||||
export function brandOf(productId: string): MockBrand | null {
|
||||
const id = PRODUCT_BRAND[productId];
|
||||
return MOCK_BRANDS.find((b) => b.id === id) ?? null;
|
||||
}
|
||||
|
||||
// ---------- stores ----------
|
||||
|
||||
export const MOCK_STORES: MockStore[] = [
|
||||
@@ -547,12 +512,20 @@ export interface MockSearchQuery {
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
// deterministic pseudo stats so sort orders are stable
|
||||
// Deterministic pseudo stats so sort orders are stable. Seeded by hashing the
|
||||
// id, not by parsing digits out of it: mock ids are "p1" but live ids are UUIDs,
|
||||
// and `Number("f8a1...")` is NaN, which rendered as "NaN sold".
|
||||
function seedOf(id: string): number {
|
||||
let hash = 0;
|
||||
for (const ch of id) hash = (hash * 31 + ch.charCodeAt(0)) % 100000;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function salesOf(p: Product): number {
|
||||
return 50 + ((Number(p.id.slice(1)) * 137) % 950);
|
||||
return 50 + ((seedOf(p.id) * 137) % 950);
|
||||
}
|
||||
export function commentCountOf(p: Product): number {
|
||||
return 5 + ((Number(p.id.slice(1)) * 61) % 240);
|
||||
return 5 + ((seedOf(p.id) * 61) % 240);
|
||||
}
|
||||
|
||||
export function searchMockProducts(query: MockSearchQuery): { items: Product[]; total: number; page: number; per_page: number } {
|
||||
@@ -585,7 +558,7 @@ export function searchMockProducts(query: MockSearchQuery): { items: Product[];
|
||||
list = [...list].sort((a, b) => dir * (commentCountOf(a) - commentCountOf(b)));
|
||||
break;
|
||||
default:
|
||||
list = [...list].sort((a, b) => Number(a.id.slice(1)) - Number(b.id.slice(1)));
|
||||
list = [...list].sort((a, b) => seedOf(a.id) - seedOf(b.id));
|
||||
}
|
||||
const total = list.length;
|
||||
const items = list.slice((page - 1) * perPage, page * perPage);
|
||||
@@ -624,20 +597,6 @@ export const MOCK_PROMOS: MockPromo[] = [
|
||||
{ image: "/mock/promo-3.svg", url: "/search?category=c6" },
|
||||
];
|
||||
|
||||
export function homeFloors(): HomeFloor[] {
|
||||
return topCategories().map((cat, i) => {
|
||||
const ids = categorySubtreeIds(cat.id);
|
||||
const products = MOCK_PRODUCTS.filter((p) => p.category_id !== null && ids.has(p.category_id)).slice(0, 8);
|
||||
return {
|
||||
categoryId: cat.id,
|
||||
name: cat.name,
|
||||
advImage: `/mock/floor-adv-${i + 1}.svg`,
|
||||
advUrl: `/search?category=${cat.id}`,
|
||||
products,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- product detail extras ----------
|
||||
|
||||
export const MOCK_COUPONS: MockCoupon[] = [
|
||||
@@ -677,7 +636,9 @@ export function commentsFor(productId: string): MockComment[] {
|
||||
}
|
||||
|
||||
export function commentStats(productId: string): { all: number; good: number; medium: number; bad: number; goodRate: number } {
|
||||
const all = commentCountOf(productById(productId) ?? MOCK_PRODUCTS[0]);
|
||||
// Derived from the id itself so it matches commentCountOf for live UUID ids
|
||||
// too, instead of falling back to an arbitrary mock product.
|
||||
const all = 5 + ((seedOf(productId) * 61) % 240);
|
||||
const good = Math.round(all * 0.92);
|
||||
const medium = Math.round(all * 0.06);
|
||||
const bad = all - good - medium;
|
||||
|
||||
Reference in New Issue
Block a user