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
@@ -0,0 +1,39 @@
# Agent contracts — mall-pc-storefront-replica
Read this whole file plus `proposal.md` and `design-reference.md` before editing.
## Hard rules
- `apps/mall` only. NEVER edit `packages/shared`, other apps, or backend.
- TS strict: no `any` / `as any` / `@ts-ignore`. Vue 3 `<script setup lang="ts">`.
- All UI copy via `$t()` / `t()`; keys live ONLY in your assigned `apps/mall/locales/<domain>.ts` file (shape: `export default { en: { <domain>: {...} }, zh: { <domain>: {...} } }`). Extend the existing structure; both en and zh always.
- Mock content fields are `LocalizedText` (`{en, zh}`); render with `t as pick` from `@vmall/shared`: `pick(text, locale)` where `const { locale } = useI18n()`.
- Money: integer minor units. Render prices ONLY via `<PriceText :amount-minor="n" currency="USD" />` (auto-imported component). No float math, no hardcoded exponent.
- Styling: scoped CSS per component using the global tokens/utilities in `apps/mall/assets/mall.css` (`--mall-red` etc., `.w1200`, `.mbtn`, `.mpanel`, `.mtable`, `.section-bg`, `.hover-lift`). No Tailwind, no element-plus, no new deps.
- Do NOT run builds/tests/dev servers; the integrator validates.
- Do NOT create/modify files outside your assigned list. If you believe a shared file must change, note it in your final report instead.
## Mock data layer (import from `~/mock/data`; read the file for full signatures)
- Shared-contract data: `MOCK_PRODUCTS: Product[]`, `MOCK_CATEGORIES: Category[]`, `MOCK_CURRENCIES`, `MOCK_USER`.
- Helpers: `searchMockProducts({q, categoryId, brandId, shopId, sort, order, page, perPage})``{items,total,page,per_page}`; `productById(idOrSlug)`; `productDetail(idOrSlug)``{product, store, comments, commentStats, coupons, salesRank}`; `lowestSku(product)`; `salesOf(p)`; `commentCountOf(p)`; `topCategories()`; `childCategories(id)`; `categorySubtreeIds(id)`; `brandOf(productId)`; `MOCK_BRANDS`.
- Stores: `MOCK_STORES: MockStore[]`, `storeById`, `storeDetail(idOrSlug)``{store, products, salesRank}`.
- Home: `MOCK_BANNERS`, `MOCK_PROMOS`, `MOCK_QUICK_LINKS`, `homeFloors()`.
- Marketing: `SECKILL_SESSIONS`, `seckillProducts()``{product, seckillPriceMinor, soldPct}[]`, `collectiveProducts()``{product, need, joined}[]`, `INTEGRAL_PRODUCTS`, banner images `/mock/integral-banner.svg`, `/mock/collective-banner.svg`.
- User center: `MOCK_ADDRESSES: MockAddress[]`, `MOCK_FAVORITES: MockFavorite[]`, `USER_STATS`, `MOCK_COUPONS`.
- Images: `/mock/*.svg` (product-1..24, banner-1..3, promo-1..3, floor-adv-1..6, store-1..4, avatar).
## API adapter (via `const { $api } = useNuxtApp()`)
Full `@vmall/shared` `ApiClient`, mock-backed with in-memory state: auth (login/register accept anything → demo user), `listProducts({page, per_page, category_id, q, shop_id})`, `getProduct`, `listCategories`, `listCurrencies`, `convert`, cart CRUD, `checkout(address, currency)``Order[]` (one per shop, clears cart), `listMyOrders`, `getOrder`, `cancelOrder`, `payOrder`, `listMyShipments`, `confirmDelivered`, `requestInvoice`, `listMyInvoices`. shop/* and admin/* throw — never call them.
Cart badge: `useCartStore()``.count`, `.refresh()` after cart mutations.
## Session
`useSessionStore()`: `isLoggedIn`, `user`, `setAuth(tokens)`, `logout()`, `hydrate()` (app.vue hydrates). Route guard: `definePageMeta({ middleware: "auth" })` on pages requiring login (user center, checkout).
## Components (auto-imported)
- Shell (already placed in app.vue — pages do NOT re-add): `ShellTopBar`, `ShellSiteHeader`, `ShellSiteFooter`.
- `UiCarousel` `{images: {image,url?}[], height?, interval?}`; `UiPagination` `{page,total,perPage}` emits `change`; `UiBreadcrumb` `{items: {label,to?}[]}` (already 1200px-wide); `UiQtyStepper` `v-model, max`; `UiRatingStars` `{value,size?}`; `UiEmptyState` `{text?}`; `UiModal` `{open,title?}` emits `close`, slots default+footer; `UiTabs` `{tabs:[{key,label}], modelValue}` emits update, default slot receives `{active}`; `UiStepBar` `{steps: string[], active: number}`; `UiProductCard` `{product: Product}`; `PriceText` `{amountMinor, currency}`; `StatusBadge` `{status, kind}` (uses shared `order.status.*` etc. keys).
## Routes (fixed)
`/` home (done); `/search` (query: q, category, brand, sort, order, page); `/goods/[id]`; `/cart`; `/checkout`, `/checkout/pay`, `/checkout/success`; `/login`, `/register`, `/forgot-password`; `/stores`, `/stores/[id]`; `/seckill`, `/collective`, `/integral`; `/user` shell + `/user` (dashboard), `/user/orders`, `/user/orders/[id]`, `/user/addresses`, `/user/favorites`, `/user/coupons`, `/user/invoices`.
## Visual reference
Follow `design-reference.md` (reference digest): 1200px grid, `#ca151e` red, `#333` dark nav, hairline `#efefef` borders, 1214px text, card hover lift, red price + struck market price, 4-step bar on transaction pages, right-floating 400px auth panel over `/mock/login-bg.svg`, user center 234px left menu + white `.mpanel` content.