feat(mall): classic B2B2C PC storefront with fixed mock API layer

- Mock adapter implementing @vmall/shared ApiClient (localStorage-persisted
  cart/orders/invoices), runtime switch via mockApi flag (default on)
- Fixed bilingual mock catalog: 3-level categories, 24 products w/ SKUs,
  stores, brands, banners, floors, seckill/collective/integral, comments,
  coupons, addresses, seeded orders/shipments/invoices
- B2B2C mall shell: top bar, logo/search/cart header, dark nav + category
  mega-menu, value-prop footer, back-to-top; 1200px grid, #ca151e theme
- UI primitives replacing element-plus: carousel, pagination, breadcrumb,
  qty stepper, rating stars, modal, tabs, step bar, product card
- Pages: home floors, search (filters/sort/paging), goods detail (SKU
  picker, store rail, review tabs), cart -> checkout -> pay -> success,
  auth pages, user center (dashboard/orders/addresses/favorites/coupons/
  invoices), stores, seckill, collective, integral
- i18n split into per-domain locale modules (en+zh)
- OpenSpec change mall-pc-storefront-replica archived; all specs green
This commit is contained in:
Chengdong Zhang
2026-09-17 19:13:29 +08:00
parent 997c312cda
commit 21e99bb52b
111 changed files with 9458 additions and 1035 deletions
+47 -105
View File
@@ -1,111 +1,53 @@
// App-local message extensions, deep-merged over @vmall/shared locales.
// Add keys here (never edit the shared locale files from an app agent).
export const enExtra = {
mall: {
catalogTitle: "Shop products",
searchPlaceholder: "Search products",
categoryPlaceholder: "Choose a category",
allCategories: "All categories",
clear: "Clear",
viewDetails: "View details",
noImage: "No image",
noProducts: "No products found",
productDescription: "Description",
attributes: "Attributes",
sku: "SKU",
chooseSku: "Choose an option",
quantity: "Quantity",
available: "available",
unavailable: "Unavailable",
addedToCart: "Added to cart",
cartItems: "Cart items",
lineTotal: "Line total",
orderPreview: "Order preview",
checkoutTitle: "Checkout",
placeOrder: "Place order",
orderSuccess: "Your order has been placed",
orderNumbers: "Order numbers",
viewOrders: "View orders",
backToShop: "Continue shopping",
noOrders: "No orders found",
created: "Created",
items: "Items",
address: "Address",
shipments: "Shipments",
invoices: "Invoices",
noInvoice: "No invoice requested",
invoiceRequested: "Invoice requested",
notAvailable: "—",
companyTaxRequired: "Tax number is required for company invoices",
invalidQuantity: "Enter a valid quantity",
loadFailed: "Unable to load data",
signInRequired: "Please sign in to continue",
filter: "Filter",
details: "Details",
update: "Update",
confirm: "Confirm",
totalItems: "Total items",
shippingOptional: "Optional",
orderDetails: "Order details",
payment: "Payment",
requestInvoiceForOrder: "Request an invoice for this order",
},
auth: {
wrongRole: "This account is not a customer account",
},
} as Record<string, unknown>;
// Each domain module owns its own namespace; register new domains below.
// (Never edit the shared locale files from an app agent.)
export const zhExtra = {
import shell from "./locales/shell";
import home from "./locales/home";
import search from "./locales/search";
import product from "./locales/product";
import cart from "./locales/cart";
import checkout from "./locales/checkout";
import auth from "./locales/auth";
import user from "./locales/user";
import stores from "./locales/stores";
import marketing from "./locales/marketing";
type Tree = Record<string, unknown>;
function deepMerge<T extends Tree>(base: T, extra: Tree): T {
const out: Tree = { ...base };
for (const [k, v] of Object.entries(extra)) {
const b = out[k];
out[k] = b && v && typeof b === "object" && typeof v === "object"
? deepMerge(b as Tree, v as Tree)
: v;
}
return out as T;
}
// Keys referenced by retained shared components (PriceText) and generic handlers.
const legacyEn: Tree = {
mall: {
catalogTitle: "选购商品",
searchPlaceholder: "搜索商品",
categoryPlaceholder: "选择分类",
allCategories: "全部分类",
clear: "清除",
viewDetails: "查看详情",
noImage: "暂无图片",
noProducts: "未找到商品",
productDescription: "商品描述",
attributes: "属性",
sku: "SKU",
chooseSku: "选择规格",
quantity: "数量",
available: "件可用",
unavailable: "暂不可用",
addedToCart: "已加入购物车",
cartItems: "购物车商品",
lineTotal: "行合计",
orderPreview: "订单预览",
checkoutTitle: "结算",
placeOrder: "提交订单",
orderSuccess: "订单提交成功",
orderNumbers: "订单号",
viewOrders: "查看订单",
backToShop: "继续购物",
noOrders: "暂无订单",
created: "创建时间",
items: "商品数",
address: "地址",
shipments: "发货信息",
invoices: "发票",
noInvoice: "尚未申请发票",
invoiceRequested: "发票已申请",
notAvailable: "—",
companyTaxRequired: "企业发票必须填写税号",
invalidQuantity: "请输入有效数量",
loadFailed: "无法加载数据",
signInRequired: "请先登录",
filter: "筛选",
details: "详情",
update: "更新",
confirm: "确认",
totalItems: "商品总数",
shippingOptional: "可选",
orderDetails: "订单详情",
payment: "支付",
requestInvoiceForOrder: "申请本订单发票",
loadFailed: "Unable to load data",
},
auth: {
wrongRole: "此账号不是买家账号",
};
const legacyZh: Tree = {
mall: {
notAvailable: "—",
loadFailed: "加载失败",
},
} as Record<string, unknown>;
};
const domains = [shell, home, search, product, cart, checkout, auth, user, stores, marketing];
export const enExtra: Tree = domains.reduce<Tree>(
(acc, m) => deepMerge(acc, m.en as Tree),
legacyEn,
);
export const zhExtra: Tree = domains.reduce<Tree>(
(acc, m) => deepMerge(acc, m.zh as Tree),
legacyZh,
);