// Fixed bilingual mock content for the mall MVP. No network, deterministic. // Money is always integer minor units + currency code (base USD). import type { Address, Brand, Category, Invoice, LocalizedText, Order, Product, Shipment, Sku, User, } from "@vmall/shared"; export const BASE_CURRENCY = "USD"; const L = (en: string, zh: string): LocalizedText => ({ en, zh }); // ---------- mall-local mock types ---------- export interface MockStore { id: string; slug: string; name: LocalizedText; company: string; logo: string; banner: string; region: string; address: LocalizedText; distanceKm: number; rate: { score: number; agree: number; service: number; speed: number }; notice: LocalizedText; afterSale: LocalizedText; } export interface MockBanner { image: string; url: string; } export interface MockQuickLink { label: LocalizedText; url: string; glyph: string; // inline SVG path data, 24x24 viewBox } export interface MockPromo { image: string; url: string; } export interface MockCoupon { id: string; title: LocalizedText; amountMinor: number; thresholdMinor: number; currency: string; expiresAt: string; } export interface MockAddress { id: string; recipient: string; phone: string; region: string; city: string; line1: string; postalCode: string; isDefault: boolean; } export interface MockFavorite { id: string; kind: "product" | "store"; refId: string; createdAt: string; } export interface SeckillSession { startHour: number; endHour: number; label: string; } export interface IntegralProduct { id: string; name: LocalizedText; image: string; points: number; marketPriceMinor: number; currency: string; stock: number; } // ---------- currencies ---------- export const MOCK_CURRENCIES = [ { code: "USD", name: L("US Dollar", "美元"), symbol: "$", exponent: 2, is_base: true, rate_to_base: "1", enabled: true }, { code: "CNY", name: L("Chinese Yuan", "人民币"), symbol: "¥", exponent: 2, is_base: false, rate_to_base: "0.14", enabled: true }, { code: "JPY", name: L("Japanese Yen", "日元"), symbol: "¥", exponent: 0, is_base: false, rate_to_base: "0.0067", enabled: true }, { code: "EUR", name: L("Euro", "欧元"), symbol: "€", exponent: 2, is_base: false, rate_to_base: "1.09", enabled: true }, ]; // units of currency per 1 USD, scaled by 1e4 (integer math only) const PER_USD: Record = { USD: 10000, CNY: 72000, JPY: 1500000, EUR: 9200 }; export function mockConvertMinor(amountMinor: number, from: string, to: string): number { if (from === to) return amountMinor; const fromRate = PER_USD[from]; const toRate = PER_USD[to]; if (!fromRate || !toRate) throw new Error(`mock convert: unknown currency ${from}/${to}`); return Math.round((amountMinor * toRate) / fromRate); } // ---------- categories (3-level tree) ---------- interface CatSpec { id: string; slug: string; name: LocalizedText; children?: CatSpec[]; } const CAT_TREE: CatSpec[] = [ { id: "c1", slug: "phones", name: L("Phones & Digital", "手机数码"), children: [ { id: "c1-1", slug: "smartphones", name: L("Smartphones", "智能手机"), children: [ { id: "c1-1-1", slug: "flagship", name: L("Flagship", "旗舰机型") }, { id: "c1-1-2", slug: "budget", name: L("Budget", "千元机") }, ], }, { id: "c1-2", slug: "audio", name: L("Audio", "影音娱乐"), children: [ { id: "c1-2-1", slug: "earbuds", name: L("Earbuds", "真无线耳机") }, { id: "c1-2-2", slug: "speakers", name: L("Speakers", "蓝牙音箱") }, ], }, ], }, { id: "c2", slug: "computers", name: L("Computers & Office", "电脑办公"), children: [ { id: "c2-1", slug: "laptops", name: L("Laptops", "笔记本电脑"), children: [ { id: "c2-1-1", slug: "ultrabook", name: L("Ultrabooks", "轻薄本") }, { id: "c2-1-2", slug: "gaming", name: L("Gaming Laptops", "游戏本") }, ], }, { id: "c2-2", slug: "peripherals", name: L("Peripherals", "外设产品"), children: [ { id: "c2-2-1", slug: "keyboards", name: L("Keyboards", "键盘") }, { id: "c2-2-2", slug: "monitors", name: L("Monitors", "显示器") }, ], }, ], }, { id: "c3", slug: "appliances", name: L("Home Appliances", "家用电器"), children: [ { id: "c3-1", slug: "kitchen", name: L("Kitchen", "厨房电器"), children: [ { id: "c3-1-1", slug: "cookers", name: L("Cookers", "电饭煲") }, { id: "c3-1-2", slug: "blenders", name: L("Blenders", "破壁机") }, ], }, { id: "c3-2", slug: "cleaning", name: L("Cleaning", "清洁电器"), children: [ { id: "c3-2-1", slug: "vacuums", name: L("Vacuums", "吸尘器") }, { id: "c3-2-2", slug: "purifiers", name: L("Air Purifiers", "空气净化器") }, ], }, ], }, { id: "c4", slug: "fashion", name: L("Fashion", "服饰鞋包"), children: [ { id: "c4-1", slug: "menswear", name: L("Menswear", "男装"), children: [ { id: "c4-1-1", slug: "jackets", name: L("Jackets", "夹克外套") }, { id: "c4-1-2", slug: "shirts", name: L("Shirts", "衬衫") }, ], }, { id: "c4-2", slug: "bags", name: L("Bags", "箱包"), children: [ { id: "c4-2-1", slug: "backpacks", name: L("Backpacks", "双肩包") }, { id: "c4-2-2", slug: "luggage", name: L("Luggage", "旅行箱") }, ], }, ], }, { id: "c5", slug: "beauty", name: L("Beauty & Care", "美妆个护"), children: [ { id: "c5-1", slug: "skincare", name: L("Skincare", "面部护肤"), children: [ { id: "c5-1-1", slug: "serums", name: L("Serums", "精华") }, { id: "c5-1-2", slug: "masks", name: L("Masks", "面膜") }, ], }, { id: "c5-2", slug: "grooming", name: L("Grooming", "个人护理"), children: [ { id: "c5-2-1", slug: "shavers", name: L("Shavers", "电动剃须刀") }, { id: "c5-2-2", slug: "toothbrush", name: L("Electric Toothbrushes", "电动牙刷") }, ], }, ], }, { id: "c6", slug: "grocery", name: L("Grocery & Fresh", "食品生鲜"), children: [ { id: "c6-1", slug: "snacks", name: L("Snacks", "休闲零食"), children: [ { id: "c6-1-1", slug: "nuts", name: L("Nuts", "坚果炒货") }, { id: "c6-1-2", slug: "pastry", name: L("Pastry", "糕点饼干") }, ], }, { id: "c6-2", slug: "fresh", name: L("Fresh Food", "生鲜果蔬"), children: [ { id: "c6-2-1", slug: "fruit", name: L("Fruit", "新鲜水果") }, { id: "c6-2-2", slug: "seafood", name: L("Seafood", "海鲜水产") }, ], }, ], }, ]; function flattenCats(specs: CatSpec[], parentId: string | null, out: Category[]): void { specs.forEach((s, i) => { out.push({ id: s.id, parent_id: parentId, name: s.name, slug: s.slug, position: i }); if (s.children) flattenCats(s.children, s.id, out); }); } export const MOCK_CATEGORIES: Category[] = (() => { const out: Category[] = []; flattenCats(CAT_TREE, null, out); return out; })(); export function categorySubtreeIds(rootId: string): Set { const ids = new Set([rootId]); let grew = true; while (grew) { grew = false; for (const c of MOCK_CATEGORIES) { if (c.parent_id && ids.has(c.parent_id) && !ids.has(c.id)) { ids.add(c.id); grew = true; } } } return ids; } // ---------- brands ---------- /** Mirrors the seeded `brands` table so the rollback path can still filter. */ export const MOCK_BRANDS: Brand[] = [ { id: "b1", slug: "aurora", name: L("Aurora", "极光"), position: 0, active: true }, { id: "b2", slug: "nordwind", name: L("Nordwind", "北风"), position: 1, active: true }, { id: "b3", slug: "hexon", name: L("Hexon", "赫克森"), position: 2, active: true }, { id: "b4", slug: "mikado", name: L("Mikado", "御门"), position: 3, active: true }, { id: "b5", slug: "solace", name: L("Solace", "索莱斯"), position: 4, active: true }, { id: "b6", slug: "terra", name: L("Terra", "大地"), position: 5, active: true }, ]; // ---------- stores ---------- export const MOCK_STORES: MockStore[] = [ { id: "s1", slug: "aurora-flagship", name: L("Aurora Flagship Store", "Aurora 官方旗舰店"), company: "Aurora Technology Co., Ltd.", logo: "/mock/store-1.svg", banner: "/mock/banner-1.svg", region: "California", address: L("1 Infinite Loop, Cupertino, CA", "加利福尼亚州库比蒂诺"), distanceKm: 3.2, rate: { score: 4.9, agree: 4.8, service: 4.9, speed: 4.7 }, notice: L("Free shipping on orders over $99.", "满 99 美元免运费。"), afterSale: L("7-day returns, 15-day exchanges, 1-year warranty.", "支持 7 天退货、15 天换货、一年质保。"), }, { id: "s2", slug: "nordwind-digital", name: L("Nordwind Digital", "北风数码专营店"), company: "Nordwind Trading LLC", logo: "/mock/store-2.svg", banner: "/mock/banner-2.svg", region: "New York", address: L("88 Fifth Ave, New York, NY", "纽约州纽约市第五大道 88 号"), distanceKm: 12.5, rate: { score: 4.7, agree: 4.6, service: 4.8, speed: 4.6 }, notice: L("Same-day dispatch before 3 PM.", "下午 3 点前下单当天发货。"), afterSale: L("Nationwide warranty, online support 9:00-21:00.", "全国联保,在线客服 9:00-21:00。"), }, { id: "s3", slug: "solace-living", name: L("Solace Living", "Solace 生活馆"), company: "Solace Home Inc.", logo: "/mock/store-3.svg", banner: "/mock/banner-3.svg", region: "Texas", address: L("500 Congress Ave, Austin, TX", "德克萨斯州奥斯汀国会大道 500 号"), distanceKm: 28.9, rate: { score: 4.8, agree: 4.9, service: 4.7, speed: 4.8 }, notice: L("New members get a welcome coupon.", "新会员可领新人券。"), afterSale: L("30-day worry-free returns on home goods.", "家居商品 30 天无忧退。"), }, { id: "s4", slug: "terra-fresh", name: L("Terra Fresh Market", "Terra 生鲜市集"), company: "Terra Foods Group", logo: "/mock/store-4.svg", banner: "/mock/banner-1.svg", region: "Washington", address: L("710 Pike St, Seattle, WA", "华盛顿州西雅图派克街 710 号"), distanceKm: 45.0, rate: { score: 4.6, agree: 4.7, service: 4.6, speed: 4.9 }, notice: L("Cold-chain delivery for fresh items.", "生鲜商品全程冷链配送。"), afterSale: L("Bad-item refund within 48 hours.", "生鲜坏单 48 小时内包赔。"), }, ]; // ---------- products ---------- interface ProductSpec { n: number; // image number slug: string; cat: string; store: string; brand: string; name: LocalizedText; sub: LocalizedText; price: number; // USD minor market: number; stock: number; attrs: { key: string; values: string[] }[]; } const PRODUCT_SPECS: ProductSpec[] = [ { n: 1, slug: "aurora-x90-pro", cat: "c1-1-1", store: "s1", brand: "b1", name: L("Aurora X90 Pro 5G", "Aurora X90 Pro 5G 手机"), sub: L("Flagship imaging, 120Hz OLED", "旗舰影像,120Hz OLED 屏"), price: 89900, market: 99900, stock: 128, attrs: [{ key: "Color", values: ["Black", "Silver"] }, { key: "Storage", values: ["256GB", "512GB"] }] }, { n: 2, slug: "aurora-lite-5g", cat: "c1-1-2", store: "s1", brand: "b1", name: L("Aurora Lite 5G", "Aurora Lite 5G 手机"), sub: L("All-day battery, great value", "长续航,高性价比"), price: 29900, market: 34900, stock: 356, attrs: [{ key: "Color", values: ["Blue", "Green"] }] }, { n: 3, slug: "hexon-buds-air", cat: "c1-2-1", store: "s2", brand: "b3", name: L("Hexon Buds Air", "Hexon Buds Air 真无线耳机"), sub: L("ANC, 36h total playback", "主动降噪,36 小时总续航"), price: 7900, market: 9900, stock: 842, attrs: [{ key: "Color", values: ["White", "Black"] }] }, { n: 4, slug: "hexon-boombox-mini", cat: "c1-2-2", store: "s2", brand: "b3", name: L("Hexon Boombox Mini", "Hexon Boombox Mini 蓝牙音箱"), sub: L("IPX7 waterproof, 20h playtime", "IPX7 防水,20 小时播放"), price: 5900, market: 7900, stock: 410, attrs: [{ key: "Color", values: ["Gray", "Red"] }] }, { n: 5, slug: "nordwind-air-14", cat: "c2-1-1", store: "s2", brand: "b2", name: L("Nordwind Air 14 Laptop", "Nordwind Air 14 轻薄本"), sub: L("1.19kg, 2.8K display, 16GB RAM", "1.19kg,2.8K 屏,16GB 内存"), price: 109900, market: 124900, stock: 96, attrs: [{ key: "Color", values: ["Silver", "Gray"] }, { key: "Memory", values: ["16GB", "32GB"] }] }, { n: 6, slug: "hexon-storm-16", cat: "c2-1-2", store: "s2", brand: "b3", name: L("Hexon Storm 16 Gaming Laptop", "Hexon Storm 16 游戏本"), sub: L("RTX graphics, 240Hz screen", "RTX 显卡,240Hz 电竞屏"), price: 189900, market: 209900, stock: 42, attrs: [{ key: "Color", values: ["Black"] }, { key: "Memory", values: ["16GB", "32GB"] }] }, { n: 7, slug: "nordwind-keys-pro", cat: "c2-2-1", store: "s2", brand: "b2", name: L("Nordwind Keys Pro", "Nordwind Keys Pro 机械键盘"), sub: L("Hot-swap, tri-mode wireless", "热插拔,三模无线"), price: 8900, market: 10900, stock: 520, attrs: [{ key: "Switch", values: ["Linear", "Tactile"] }] }, { n: 8, slug: "nordwind-view-27", cat: "c2-2-2", store: "s2", brand: "b2", name: L("Nordwind View 27 Monitor", "Nordwind View 27 显示器"), sub: L("4K IPS, 99% sRGB", "4K IPS,99% sRGB 色域"), price: 32900, market: 39900, stock: 150, attrs: [{ key: "Size", values: ["27in", "32in"] }] }, { n: 9, slug: "solace-rice-chef", cat: "c3-1-1", store: "s3", brand: "b5", name: L("Solace Rice Chef 4L", "Solace 智能电饭煲 4L"), sub: L("IH heating, 24h timer", "IH 电磁加热,24 小时预约"), price: 12900, market: 15900, stock: 230, attrs: [{ key: "Color", values: ["White"] }] }, { n: 10, slug: "solace-blend-max", cat: "c3-1-2", store: "s3", brand: "b5", name: L("Solace Blend Max", "Solace 破壁机"), sub: L("Quiet motor, self-cleaning", "静音电机,一键自清洗"), price: 14900, market: 18900, stock: 178, attrs: [{ key: "Color", values: ["Cream", "Green"] }] }, { n: 11, slug: "solace-vac-v12", cat: "c3-2-1", store: "s3", brand: "b5", name: L("Solace Vac V12", "Solace V12 无线吸尘器"), sub: L("60min runtime, laser dust detect", "60 分钟续航,激光显尘"), price: 39900, market: 45900, stock: 88, attrs: [{ key: "Color", values: ["Gray"] }] }, { n: 12, slug: "solace-pure-500", cat: "c3-2-2", store: "s3", brand: "b5", name: L("Solace Pure 500 Air Purifier", "Solace Pure 500 空气净化器"), sub: L("HEPA-13, 60m² coverage", "HEPA-13 滤芯,适用 60㎡"), price: 24900, market: 29900, stock: 140, attrs: [{ key: "Color", values: ["White"] }] }, { n: 13, slug: "mikado-field-jacket", cat: "c4-1-1", store: "s1", brand: "b4", name: L("Mikado Field Jacket", "Mikado 工装夹克"), sub: L("Water-repellent, 4 pockets", "防泼水面料,四袋设计"), price: 11900, market: 14900, stock: 310, attrs: [{ key: "Color", values: ["Olive", "Black"] }, { key: "Size", values: ["M", "L", "XL"] }] }, { n: 14, slug: "mikado-oxford-shirt", cat: "c4-1-2", store: "s1", brand: "b4", name: L("Mikado Oxford Shirt", "Mikado 牛津纺衬衫"), sub: L("Wrinkle-free cotton", "免烫纯棉"), price: 4900, market: 6900, stock: 460, attrs: [{ key: "Color", values: ["White", "Blue"] }, { key: "Size", values: ["M", "L", "XL"] }] }, { n: 15, slug: "mikado-commuter-pack", cat: "c4-2-1", store: "s1", brand: "b4", name: L("Mikado Commuter Pack 20L", "Mikado 通勤双肩包 20L"), sub: L("Fits 16in laptop, USB port", "可装 16 英寸电脑,外置 USB 口"), price: 6900, market: 8900, stock: 275, attrs: [{ key: "Color", values: ["Black", "Navy"] }] }, { n: 16, slug: "mikado-voyage-24", cat: "c4-2-2", store: "s1", brand: "b4", name: L("Mikado Voyage 24 Luggage", "Mikado Voyage 24 英寸旅行箱"), sub: L("PC shell, silent wheels", "PC 箱体,静音万向轮"), price: 13900, market: 16900, stock: 132, attrs: [{ key: "Color", values: ["Silver", "Black"] }] }, { n: 17, slug: "solace-glow-serum", cat: "c5-1-1", store: "s3", brand: "b5", name: L("Solace Glow Serum 30ml", "Solace 焕亮精华 30ml"), sub: L("Vitamin C + E, daily glow", "维 C + 维 E,日常焕亮"), price: 3900, market: 4900, stock: 640, attrs: [{ key: "Size", values: ["30ml", "50ml"] }] }, { n: 18, slug: "solace-hydra-mask", cat: "c5-1-2", store: "s3", brand: "b5", name: L("Solace Hydra Mask 10pcs", "Solace 补水面膜 10 片装"), sub: L("Hyaluronic acid boost", "玻尿酸深层补水"), price: 1900, market: 2900, stock: 900, attrs: [{ key: "Type", values: ["Hydrating", "Soothing"] }] }, { n: 19, slug: "hexon-shave-s9", cat: "c5-2-1", store: "s2", brand: "b3", name: L("Hexon Shave S9", "Hexon S9 电动剃须刀"), sub: L("3-head flex, wet & dry", "三刀头浮动,干湿两用"), price: 9900, market: 12900, stock: 205, attrs: [{ key: "Color", values: ["Black"] }] }, { n: 20, slug: "hexon-brush-t5", cat: "c5-2-2", store: "s2", brand: "b3", name: L("Hexon Brush T5", "Hexon T5 电动牙刷"), sub: L("Sonic, 90-day battery", "声波震动,90 天续航"), price: 4900, market: 6900, stock: 380, attrs: [{ key: "Color", values: ["White", "Pink"] }] }, { n: 21, slug: "terra-mixed-nuts", cat: "c6-1-1", store: "s4", brand: "b6", name: L("Terra Mixed Nuts 1kg", "Terra 混合坚果 1kg"), sub: L("6 kinds, no added salt", "6 种坚果,无添加盐"), price: 2900, market: 3900, stock: 720, attrs: [{ key: "Pack", values: ["1kg", "500g"] }] }, { n: 22, slug: "terra-butter-cookies", cat: "c6-1-2", store: "s4", brand: "b6", name: L("Terra Butter Cookies 908g", "Terra 黄油曲奇 908g"), sub: L("Danish recipe, gift tin", "丹麦配方,礼盒铁罐装"), price: 2400, market: 3200, stock: 540, attrs: [{ key: "Pack", values: ["908g"] }] }, { n: 23, slug: "terra-fuji-apples", cat: "c6-2-1", store: "s4", brand: "b6", name: L("Terra Fuji Apples 5kg", "Terra 红富士苹果 5kg"), sub: L("Crisp & sweet, farm direct", "脆甜多汁,产地直发"), price: 1900, market: 2600, stock: 460, attrs: [{ key: "Size", values: ["Medium", "Large"] }] }, { n: 24, slug: "terra-salmon-fillet", cat: "c6-2-2", store: "s4", brand: "b6", name: L("Terra Salmon Fillet 500g", "Terra 三文鱼中段 500g"), sub: L("Cold-chain, sashimi grade", "冷链直达,刺身级"), price: 3400, market: 4200, stock: 190, attrs: [{ key: "Cut", values: ["Fillet", "Steak"] }] }, ]; function buildProducts(): Product[] { const now = "2026-09-01T00:00:00.000Z"; return PRODUCT_SPECS.map((spec, i) => { const id = `p${i + 1}`; const img = `/mock/product-${spec.n}.svg`; const skus: Sku[] = []; const combos = spec.attrs.reduce( (acc, attr) => acc.flatMap((prefix) => attr.values.map((v) => [...prefix, `${attr.key}:${v}`])), [[]], ); combos.forEach((combo, k) => { const attributes: Record = {}; for (const pair of combo) { const idx = pair.indexOf(":"); attributes[pair.slice(0, idx)] = pair.slice(idx + 1); } skus.push({ id: `${id}-sku${k + 1}`, product_id: id, sku_code: `${spec.slug.toUpperCase().replaceAll("-", "_")}_${k + 1}`, attributes, price_minor: spec.price + k * 1000, currency: BASE_CURRENCY, stock: Math.max(0, Math.floor(spec.stock / combos.length) - (k % 3)), active: true, }); }); return { id, shop_id: spec.store, category_id: spec.cat, brand_id: spec.brand, slug: spec.slug, name: spec.name, description: spec.sub, images: [img, img, img], status: "published", created_at: now, // The fixed-data path has no order data to count, so it reports none // rather than inventing a figure. sold_count: 0, skus, } satisfies Product; }); } export const MOCK_PRODUCTS: Product[] = buildProducts(); export function productById(idOrSlug: string): Product | null { return MOCK_PRODUCTS.find((p) => p.id === idOrSlug || p.slug === idOrSlug) ?? null; } export function storeById(idOrSlug: string): MockStore | null { return MOCK_STORES.find((s) => s.id === idOrSlug || s.slug === idOrSlug) ?? null; } import { lowestSku } from "~/utils/product"; /** Kept for the mock-era pages; live pages import it from `~/utils/product`. */ export { lowestSku }; export interface MockSearchQuery { q?: string; categoryId?: string; brandId?: string; shopId?: string; sort?: "default" | "price" | "sales"; order?: "asc" | "desc"; page?: number; perPage?: number; } /** Stable ordering for the fixed-data catalogue; not a claim about sales. */ function seedOf(id: string): number { let hash = 0; for (const ch of id) hash = (hash * 31 + ch.charCodeAt(0)) % 100000; return hash; } export function searchMockProducts(query: MockSearchQuery): { items: Product[]; total: number; page: number; per_page: number } { const page = query.page && query.page > 0 ? query.page : 1; const perPage = query.perPage && query.perPage > 0 ? query.perPage : 20; let list = MOCK_PRODUCTS.filter((p) => p.status === "published"); if (query.shopId) list = list.filter((p) => p.shop_id === query.shopId); if (query.categoryId) { const ids = categorySubtreeIds(query.categoryId); list = list.filter((p) => p.category_id !== null && ids.has(p.category_id)); } if (query.brandId) list = list.filter((p) => p.brand_id === query.brandId); const kw = query.q?.trim().toLowerCase(); if (kw) { list = list.filter((p) => [p.name.en, p.name.zh, p.description.en, p.description.zh] .filter((v): v is string => typeof v === "string") .some((v) => v.toLowerCase().includes(kw)), ); } const dir = query.order === "asc" ? 1 : -1; switch (query.sort) { case "price": list = [...list].sort((a, b) => dir * ((lowestSku(a)?.price_minor ?? 0) - (lowestSku(b)?.price_minor ?? 0))); break; case "sales": // The fixed-data catalogue has no orders, so every product reports zero // sold and this keeps the incoming order rather than inventing one. list = [...list].sort((a, b) => dir * (a.sold_count - b.sold_count)); break; default: list = [...list].sort((a, b) => seedOf(a.id) - seedOf(b.id)); } const total = list.length; const items = list.slice((page - 1) * perPage, page * perPage); return { items, total, page, per_page: perPage }; } // ---------- home content ---------- export const MOCK_BANNERS: MockBanner[] = [ { image: "/mock/banner-1.svg", url: "/search?sort=sales&order=desc" }, { image: "/mock/banner-2.svg", url: "/seckill" }, { image: "/mock/banner-3.svg", url: "/collective" }, ]; const GLYPHS = { shield: "M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-4z", coin: "M12 2a10 10 0 100 20 10 10 0 000-20zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z", gift: "M20 7h-3.2A3 3 0 0012 3.5 3 3 0 007.2 7H4v4h7V9h2v2h7V7zM4 13v8h7v-8H4zm9 8h7v-8h-7v8z", bolt: "M13 2L4 14h6l-1 8 9-12h-6l1-8z", bell: "M12 22a2.5 2.5 0 002.5-2.5h-5A2.5 2.5 0 0012 22zm7-6v-5a7 7 0 00-5-6.7V4a2 2 0 00-4 0v.3A7 7 0 005 11v5l-2 2v1h18v-1l-2-2z", shop: "M4 4h16l2 5v2a3 3 0 01-3 3 3 3 0 01-3-3 3 3 0 01-3 3 3 3 0 01-3-3 3 3 0 01-3 3 3 3 0 01-3-3V9l2-5zm0 10.7V20h7v-5h2v5h7v-5.3a4.98 4.98 0 01-2 .4V20H6v-5.7a4.98 4.98 0 01-2 .4z", }; export const MOCK_QUICK_LINKS: MockQuickLink[] = [ { label: L("Verification", "实名认证"), url: "/user", glyph: GLYPHS.shield }, { label: L("Points Mall", "积分商城"), url: "/integral", glyph: GLYPHS.coin }, { label: L("Group Buy", "优惠团购"), url: "/collective", glyph: GLYPHS.gift }, { label: L("Flash Sale", "秒杀活动"), url: "/seckill", glyph: GLYPHS.bolt }, { label: L("Notice", "商城公告"), url: "/user", glyph: GLYPHS.bell }, { label: L("Become a Seller", "入驻商家"), url: "/stores", glyph: GLYPHS.shop }, ]; export const MOCK_PROMOS: MockPromo[] = [ { image: "/mock/promo-1.svg", url: "/search?category=c1" }, { image: "/mock/promo-2.svg", url: "/search?category=c3" }, { image: "/mock/promo-3.svg", url: "/search?category=c6" }, ]; // ---------- product detail extras ---------- export const MOCK_COUPONS: MockCoupon[] = [ { id: "cp1", title: L("$5 off over $59", "满 59 减 5"), amountMinor: 500, thresholdMinor: 5900, currency: BASE_CURRENCY, expiresAt: "2026-12-31" }, { id: "cp2", title: L("$15 off over $199", "满 199 减 15"), amountMinor: 1500, thresholdMinor: 19900, currency: BASE_CURRENCY, expiresAt: "2026-11-30" }, { id: "cp3", title: L("$40 off over $499", "满 499 减 40"), amountMinor: 4000, thresholdMinor: 49900, currency: BASE_CURRENCY, expiresAt: "2026-10-31" }, ]; // ---------- marketing ---------- export const SECKILL_SESSIONS: SeckillSession[] = [ { startHour: 0, endHour: 8, label: "00:00" }, { startHour: 8, endHour: 12, label: "08:00" }, { startHour: 12, endHour: 18, label: "12:00" }, { startHour: 18, endHour: 24, label: "18:00" }, ]; export function seckillProducts(): { product: Product; seckillPriceMinor: number; soldPct: number }[] { return MOCK_PRODUCTS.slice(0, 8).map((p, i) => { const sku = lowestSku(p); const base = sku?.price_minor ?? 1000; return { product: p, seckillPriceMinor: Math.round(base * (0.5 + (i % 3) * 0.1)), soldPct: 20 + ((i * 17) % 75), }; }); } export function collectiveProducts(): { product: Product; need: number; joined: number }[] { return MOCK_PRODUCTS.slice(8, 16).map((p, i) => ({ product: p, need: 2 + (i % 3), joined: 10 + ((i * 23) % 80), })); } export const INTEGRAL_PRODUCTS: IntegralProduct[] = [ { id: "ip1", name: L("Insulated Mug", "保温杯"), image: "/mock/product-9.svg", points: 1200, marketPriceMinor: 4900, currency: BASE_CURRENCY, stock: 300 }, { id: "ip2", name: L("Canvas Tote", "帆布手提袋"), image: "/mock/product-15.svg", points: 600, marketPriceMinor: 1900, currency: BASE_CURRENCY, stock: 500 }, { id: "ip3", name: L("Phone Stand", "手机支架"), image: "/mock/product-7.svg", points: 400, marketPriceMinor: 1200, currency: BASE_CURRENCY, stock: 800 }, { id: "ip4", name: L("Cable Organizer Set", "数据线收纳套装"), image: "/mock/product-3.svg", points: 300, marketPriceMinor: 900, currency: BASE_CURRENCY, stock: 1000 }, { id: "ip5", name: L("Notebook A5", "A5 笔记本"), image: "/mock/product-14.svg", points: 250, marketPriceMinor: 700, currency: BASE_CURRENCY, stock: 1200 }, { id: "ip6", name: L("Umbrella", "折叠雨伞"), image: "/mock/product-13.svg", points: 900, marketPriceMinor: 2900, currency: BASE_CURRENCY, stock: 260 }, ]; // ---------- user center ---------- export const MOCK_USER: User = { id: "u1", email: "shopper@vmall.dev", display_name: "Demo Shopper", role: "customer", shop_id: null, locale: "en", created_at: "2026-01-15T08:00:00.000Z", }; export const MOCK_ADDRESSES: MockAddress[] = [ { id: "a1", recipient: "Demo Shopper", phone: "+1 555 0100", region: "California", city: "Cupertino", line1: "1 Infinite Loop", postalCode: "95014", isDefault: true }, { id: "a2", recipient: "Demo Shopper", phone: "+1 555 0100", region: "New York", city: "New York", line1: "88 Fifth Ave", postalCode: "10011", isDefault: false }, ]; export function defaultAddress(): Address { const a = MOCK_ADDRESSES.find((x) => x.isDefault) ?? MOCK_ADDRESSES[0]; return { recipient: a.recipient, phone: a.phone, country: "US", region: a.region, city: a.city, line1: a.line1, postal_code: a.postalCode, }; } export const MOCK_FAVORITES: MockFavorite[] = [ { id: "f1", kind: "product", refId: "p1", createdAt: "2026-08-20" }, { id: "f2", kind: "product", refId: "p5", createdAt: "2026-08-25" }, { id: "f3", kind: "store", refId: "s1", createdAt: "2026-09-01" }, ]; export const USER_STATS = { balanceMinor: 12800, points: 2680, frozenMinor: 0 }; export interface DashboardCounts { pendingPayment: number; pendingShipment: number; shipped: number; pendingComment: number; afterSale: number; } // ---------- seeded orders / shipments / invoices ---------- function orderItemFrom(productIndex: number, qty: number): { id: string; sku: Sku; product: Product; qty: number } { const product = MOCK_PRODUCTS[productIndex]; const sku = lowestSku(product) ?? product.skus?.[0]; if (!sku) throw new Error("mock seed: product has no sku"); return { id: `oi-${productIndex + 1}`, sku, product, qty }; } export interface MockOrderSeed { orders: Order[]; shipments: Shipment[]; invoices: Invoice[]; } export function seedOrders(userId: string): MockOrderSeed { const mk = ( id: string, orderNo: string, picks: number[], status: Order["status"], createdAt: string, ): Order => { const items = picks.map((pi, k) => { const { sku, product, qty } = orderItemFrom(pi, k + 1); return { id: `${id}-it${k + 1}`, sku_id: sku.id, product_name: product.name, sku_code: sku.sku_code, unit_price_minor: sku.price_minor, qty, image: product.images[0] ?? null, flash_sale_item_id: null, }; }); const total = items.reduce((sum, it) => sum + it.unit_price_minor * it.qty, 0); return { id, order_no: orderNo, shop_id: MOCK_PRODUCTS[picks[0]].shop_id, user_id: userId, status, currency: BASE_CURRENCY, total_minor: total, discount_minor: 0, coupon_id: null, group_activity_id: null, group_id: null, items, shipping_address: defaultAddress(), created_at: createdAt, }; }; const orders: Order[] = [ mk("o1", "VM20260910001", [0], "shipped", "2026-09-10T10:00:00.000Z"), mk("o2", "VM20260912002", [2, 6], "completed", "2026-09-12T15:30:00.000Z"), mk("o3", "VM20260915003", [20], "pending_payment", "2026-09-15T09:12:00.000Z"), ]; const shipments: Shipment[] = [ { id: "sh1", shipment_no: "SH20260910001", order_id: "o1", order_no: "VM20260910001", carrier: "SF Express", tracking_no: "SF1000000001", status: "shipped", items: [{ order_item_id: "o1-it1", qty: 1 }], shipped_at: "2026-09-10T18:00:00.000Z", delivered_at: null, created_at: "2026-09-10T17:30:00.000Z", }, ]; const invoices: Invoice[] = [ { id: "inv1", invoice_no: "INV20260912001", order_id: "o2", order_no: "VM20260912002", title: "Demo Shopper", tax_no: null, kind: "personal", amount_minor: orders[1].total_minor, currency: BASE_CURRENCY, status: "issued", issued_at: "2026-09-13T09:00:00.000Z", created_at: "2026-09-12T16:00:00.000Z", }, ]; return { orders, shipments, invoices }; }