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.
@@ -0,0 +1,139 @@
# PC buyer mall reference digest
## 1. Home route table
`resources/js/plugins/routes/index.js` defines one parent `path: '/'` component `Home/index` (the global buyer shell). Its children are:
| Path | Page file | Purpose / mock data boundary |
|---|---|---|
| `/` | `Home/default.vue` | Home landing page: hero/banner, quick links and category product floors. `/home` response supplies `goods` groups; each group has `name`, `goods[]`, and each product has `id`, `goods_master_image`, `goods_name`, `goods_subname`, `goods_price`, `goods_market_price`. |
| `/login` | `Home/auth/login.vue` | Username/password login plus WeChat login. |
| `/register` | `Home/auth/register.vue` | Phone registration, password confirmation and SMS code. |
| `/forget_password` | `Home/auth/forget_password.vue` | Phone, new password/confirmation and SMS code reset. |
| `/stores` | `Home/stores/list.vue` | Store directory, default/distance sort, pagination. |
| `/store/:id` | `Home/stores/info.vue` | Store profile/rating, store sales rail, product listing/sort. |
| `/store/join` | `Home/stores/index.vue` | Merchant onboarding landing; links into join steps. |
| `/store/step_1`, `/store/step_2`, `/store/step_3` | corresponding `Home/stores/step_*.vue` | Join agreement/data/review state steps. |
| `/s/:params?` | `Home/goods/index.vue` | Product search/list; optional base64 JSON query (`keywords`, `pid/sid/tid`, `class_id`, `brand_id`, `sort_type`, `sort_order`). |
| `/goods/:id` | `Home/goods/info.vue` | Product detail, gallery/zoom, SKU, buy/cart, store, detail/comments/service. |
| `/carts` | `Home/carts/index.vue` | Cart grouped by store, selection/quantity/removal and checkout. |
| `/integral` | `Home/integral/index.vue` | Points-mall home (banner, account panel, recommendations/category rails). |
| `/integral/search/:params?` | `Home/integral/goods.vue` | Points product search/category/sort list. |
| `/integral/goods/:id` | `Home/integral/info.vue` | Points product detail and quantity selection. |
| `/integral/order/:id/:buy_num` | `Home/integral/orders/index.vue` | Address + points redemption order. |
| `/seckills` | `Home/seckills/index.vue` | Timed seckill sessions/products. |
| `/collectives/:params?` | `Home/collectives/index.vue` | Group-buy product list, using ordinary goods filters with `is_collective: 1`. |
| `/user` | `Home/users/index.vue` with child `Home/users/default.vue` | Buyer center shell and dashboard. |
| `/user/address` (+ `/form/:id?`) | `Home/users/address/index.vue` (+ `form.vue`) | Address list and add/edit. |
| `/user/comments`, `/user/comment/add/:id` | comments pages | Buyer comments and add-comment flow. |
| `/user/info`, `/user/safe/**`, `/user/oauth`, `/user/cashes` | user subpages | Profile, safety/password/phone, OAuth binding and withdrawal. |
| `/user/favorites` | `Home/users/favorites/index.vue` | Toggle product favorites/store follows. |
| `/user/money`, `/user/integral`, `/user/frozen_money` | money-log pages | Balance, points and frozen-funds records. |
| `/user/orders`, `/user/order/:id`, `/user/order/comment/:id`, `/user/order/refund/**` | order subpages | Order list/detail/comment/refund flows. |
| `/user/integral_order` | integral-order page | Users points orders. |
| `/user/coupons` | `Home/users/coupons/index.vue` | Coupon table. |
| `/user/distribution/**` | distribution pages | Distribution profile/members/commissions. |
| `/user/article/:name` | article page | Named announcement/help/about content. |
| `/order/before/:params` | `Home/orders/before.vue` | Address/coupon/remark and order preview. |
| `/order/pay/:params` | `Home/orders/pay.vue` | Payment selection and generated order summary. |
| `/order/success` | `Home/orders/success.vue` | Completed payment result. |
`views/App.vue` is only `<router-view/>`; `/Admin/login` and `/Seller/login` are separate non-buyer routes (Admin/Seller areas exist but are out of this survey). `router.js` creates a history router, installs `baseRoute`, and has global progress/auth navigation logic.
## 2. Global layout anatomy
The naming is slightly split: `views/Home/index.vue` is the shell, `views/Home/default.vue` is the actual home landing page, and `components/home/base.vue` is the shared outer layout.
1. **`components/home/top.vue` fixed top bar (`.head`, 30px):** 1200px `.head_in`; left links in order: 平台官网, separator, 商城官网, separator, 联系方式, separator, 关于我们, separator, city/location (default 北京市, optionally IP/AMap-resolved). Right side conditionally shows 登录/注册 or `欢迎您,nickname`, 个人中心, 注销账号; then 商家入驻, 手机端, APP下载. Uses `#ca151e` for login/location/merchant links.
2. **`components/home/head.vue` fixed header (`top:30px`, white):** `.center_top center1200` logo (`.shop_logo`, 242x74), then `.shop_top_seach` with 434px text input (`data.keywords`), search icon button and `.index_my_car` “我的商城/我的购物车” with cart count badge `.shop_car_dot`. Below is 40px dark `.shop_top_nav`: 240px red `.shop_top_nav_left` “全部商品” (hover mounts `leftbar`), then links 首页, 店铺街, 秒杀, 拼团, 积分商城, 帮助中心. Header collapses logo/search area after scroll >200; home category menu can collapse after scroll. Search pushes `/s/` + base64 JSON `{keywords}`.
3. **`components/home/leftbar.vue` category mega-menu:** 240px x 450px `.leftbar` (white) or `.leftbar2` (black translucent when over hero). Each top-level `goodsClass` renders `.left_bar_block`: `.class_1` top category name, `.class_2` up to three child names, and hidden hover `.subbar` 960px wide. Mega menu contains optional top tags, right brand tiles from `goodsBrand[]` (`thumb`), optional advert, and `.subbar_subnav` rows: child heading `name`, grandchildren `{id,pid,name}`. Navigation encodes `pid/sid/tid/class_id` into `/s/:params`.
4. **`components/home/base.vue`:** `<div class="home_default"><top/><div class="padding_content"/><slot name="main"/><foot/></div>`. `.padding_content` is 228px to offset fixed top + header. On mount dispatches `init/loadCommon` (common config, classes, brands, cart, etc.). In `Home/index.vue`, the main slot is `<head-view/><router-view/>`; therefore every buyer page receives the top/header/footer shell. Home `default.vue` itself is just the route content under that shell.
5. **`components/home/foot.vue`:** centered 1200px `.shop_foot`; four value propositions in order (“500强企业 品质保证”, “7天退货 15天换货”, “99元起免运费”, “448家维修网点 全国联保”), then copyright/ICP/security text interpolating `common.web_name` and `common.icp`, plus Element `el-backtop`. Replace `el-backtop` with a small fixed scroll-to-top control in Nuxt.
Element Plus pieces to replace in this shell: `el-icon`/icons (search, cart, etc.), `el-image` (native img with lazy loading), `el-carousel`/transition (CSS/Headless carousel), `el-backtop`, and any `el-breadcrumb` used by child pages.
## 3. Homepage sections and mock data
`views/Home/default.vue` is intentionally simpler than the nav labels suggest—there are no dedicated seckill/collective/brand sections in this file. Those are quick links in `banner_adv`; product floors are generic category groups.
1. **Hero carousel (`components/home/banner.vue`, first child):** `<el-carousel height=450>` → each `<el-carousel-item>``.shop_banner` background image (`background-position:center; background-size:cover`) → full 1200x450 router link. `banner` shape: `[{image:string, url?:string, name?:string}]`; default landing mock is one `default_banner.jpg` item. Nuxt replacement: regular CSS/JS carousel.
2. **Below-hero promo/quick-link strip (`components/home/banner_adv.vue`):** `.banner_bottom_adv center1200`; left 240px six 78x80 cells in order: 实名认证 (`/user/safe`), 积分商城, 优惠团购, 秒杀活动, 公告 (`/user`), 入驻商家. Each icon is an image; right side has up to three 312x160 linked promo images. `index_adv` shape: `[{image:string, url?:string, name?:string}]`. Uses `el-image` (replace with img).
3. **Floor background:** `.index_bg` gray `#f5f5f5`, top margin 40px and 20px/60px padding. For every `data.goods` category group, render `.goods_class_list w1200`; if `v.goods.length > 0`, first `adv-view` full-width placeholder (`adv:{image,url?}`), then `.title` group name + “查看更多”.
4. **Floor body:** left `.index_adv_goods_left` 234x614 image from `data.classLeftAdv[k].image_url` or fallback `pc_class_btadv.jpg`; right `.index_class_goods_right` 966px with product cards in `<ul>`. Product cards are 220x300 white `.product_act_in` with 140x140 image, title, subtitle, red sale price and struck market price. Floor product shape: `{id, goods_master_image, goods_name, goods_subname, goods_price, goods_market_price}`; floor group shape: `{name, goods:[product...]}`. Note `classLeftAdv` is initialized but not assigned from `/home` in the shown code, so a mock can supply it separately.
5. **`components/home/adv.vue`:** a linked lazy image, default `width/height:'100%'`, margin top/bottom 40px; `adv:{image,url?}`.
Visual card behavior: hover lifts cards by 3px and adds `0 2px 12px rgba(0,0,0,.1)`. A Nuxt mock module should include `home {goods: Floor[], classLeftAdv: Adv[]}`, `banner`, and `index_adv` even though the current banner/floor images are hardcoded assets.
## 4. Goods list (`views/Home/goods/index.vue`)
- Breadcrumb `.w1200 breadcrumb`: 首页, then either `搜索 "..." 结果列表` (decoded `keywords`) or 商品列表.
- **Filter block `.goods_where w1200`:** three `.item` rows: 商品分类, 品牌筛选, 筛选排序. Category has “全部”, top-level `common.classes`, selected second-level and third-level children; each class is `{id,name,children:[...]}`. Brand options are `{id,name}`. The route state/query shape is `{keywords?,pid?,sid?,tid?,class_id?:number[],brand_id?,sort_type?,sort_order?}` encoded in base64 path.
- **Important reference limitation:** despite a request to survey “attrs/brands/price”, this template has no attribute facet row and no price-range controls. It exposes category + brands + sort only. Do not infer an attrs/price UI from this file; if the new app adds them, it is an intentional enhancement rather than parity.
- Sort choices are default, 价格 (`goods_price`), 销量 (`goods_sale`), 评论 (`order_comment_count`), each toggling `sort_order` asc/desc. In the shown template the sort controls are also in `.goods_where`, not a separate bar.
- **Results:** `.s_goods_content`; `.s_goods_list` five cards/row (`.item` 224x364). Each card `<dl>` has 176x176 image, title, red price, footer “立即购买” and `{order_comment_count} 人评论`; links `/goods/{id}`. Product list shape: `{id,goods_master_image,goods_name,goods_price,order_comment_count}` (market/subname are not rendered here).
- **Pagination:** `.fy` Element `el-pagination` total/prev/pager/next; response fields are `data`, `total`, `per_page`, `last_page`, `current_page`; empty state is `el-empty`. Replace with plain button/page-number pagination, empty state, and native img.
## 5. Goods detail (`views/Home/goods/info.vue`)
- **Breadcrumb:** `.mbx w1200`: 首页 → every `goods_info.classList[{name}]` → product name.
- **Gallery:** `.goods_info_top_left` has `.goods_image_item` containing custom `components/home/vue-piczoom.vue`, then thumbnail rail `.pic_zoom_list` with previous/next controls and `goods_images_thumb_150[]`; selected index `chose_img_pos`, display image `goods_images_thumb_400[]`, zoom source `goods_images[]`. Mock fields: `goods_images`, `goods_images_thumb_400`, `goods_images_thumb_150` arrays of URLs.
- **Product summary:** `.goods_info_top_right` title/subtitle, favorite toggle, sale/market prices, sale count, QR hint, coupon cards, optional seckill and group-buy activity, full-reduction messages. Core `goods_info`: `id,goods_name,goods_subname,goods_price,goods_market_price,goods_sale,goods_stock,goods_content,classList,attrList,skuList,sale_list`; `coupons[]` entries `{id,money,use_money}`, `full_reductions[]` `{money,use_money}`, `seckills` `{discount,end_time,format_time}`, `collectives` `{discount,need}`, `collective_list[]` `{id,avatar,nickname,orders_count,need}`.
- **SKU/quantity:** `attrList[]` each `{name,specs:[{id,name,is_chose?}]}`; `skuList[]` each `{id,spec_id:number[],goods_price,goods_market_price,goods_stock}`. Chosen `sku_id`, `buy_num`, current stock; “立即购买”, “加入购物车” (regular goods) or “立即抢购” (seckill). Group buy adds `collective_id`.
- **Store card / left rail:** `.goods_info_content` left `.left_item` store card with `store_info.store_name`, `store_company_name`, `area_info`, `store_address`, `id`, `user_id`; four scores in `rate_info` (`scoreAll`, `agreeAll`, `serviceAll`, `speedAll`); buttons enter store/customer service. Below, 销售排行 from `goods_info.sale_list[]` product `{id,goods_master_image,goods_name,goods_price}`.
- **Tabs:** right `.right_item` Element tabs: 商品详情 renders sanitized `goods_content`; 用户评价 renders statistics `{all,good,commonly,bad,rate}`, comment filters, paginated `comments[]` where `{avatar,nickname,score,content,image:string[],reply}`; 售后服务 renders `store_info.after_sale_service`. Replace `el-tabs`, `el-rate`, `el-pagination`, `el-image` with native tabs, stars and controls. `Chat` is a separate common component and may be replaced with a simple contact CTA if chat is not in scope.
## 6. Cart and order flow
**Cart (`views/Home/carts/index.vue`):** `.create_order_1 w1200` begins a four-step `.step_bar` (cart active, address, payment, success). If nonempty, `.cart_th` header columns are 商品, 规格, 单价(元), 数量, 小计, 操作. `.cart_table` groups `data.list` by store: store group `{store_id,store_name,checked,css,cart_list[]}`, item `{cart_id,goods_id,goods_image,goods_name,sku_name,goods_price,buy_num,checked}`. Each item has checkbox, 40px thumb/name link, SKU, +/- quantity controls, subtotal and 移除. Bottom select-all summarizes `allCount`, `allPrice` and 结算; empty `el-empty`. Pagination response state exists (`data.params={page,per_page,last_page,total}`) but no pager is rendered. Replace Element checkboxes/empty/icons.
**Order before (`orders/before.vue`):** same step bar with cart/address active. Address selection list entries `{id,receive_name,receive_tel,area_info,address,is_default}`; then order preview grouped by store. Order group includes `store_info:{id,store_logo,store_name}`, `coupon_id`, `coupons[]:{id,money}`, `is_coupon`, `goods_list[]:{goods_master_image,goods_name,sku_name,goods_price,buy_num,total}`. Shows remark textarea (`data.remark`), total, create-order button. Submit payload uses encoded route `params`, `address_id`, comma-joined `coupon_id`, `remark`.
**Order pay (`orders/pay.vue`):** step bar through payment; balance panel `userInfo.money`; payment options are balance/password, WeChat and Alipay. Displays generated `order[]` groups `{order_no,coupon_money,order_goods[]:{goods_image,goods_name,sku_name,goods_price,buy_num,total_price}}`, `total`, `freight_money`; balance password and QR modal state `{pay_password,qr_code}`. Replace `el-dialog`, QR Vue and Element grid with Tailwind modal/grid and a QR library.
**Order success (`orders/success.vue`):** all four steps active, success result (“订单支付成功!”), link to `/user`; replace Element `el-result` with a simple success panel.
## 7. User center
`Home/users/index.vue` is a two-column `.user_default` shell: 234px `.user_left` + 20px gap, 946px `.user_right` with nested `<router-view>`. Profile card has avatar, `nickname`, edit link, account-profile progress (`check`) and safety indicators (`phone`, `check`). Nav menu groups/items, in order:
- **订单中心:** 我的订单 `/user/orders`, 收货地址 `/user/address`, 评论列表 `/user/comments`.
- **会员中心:** 个人中心 `/user`, 用户资料 `/user/info`, 账户安全 `/user/safe`, 账号绑定 `/user/oauth`, 资金提现 `/user/cashes`, 收藏/关注 `/user/favorites`, 我的优惠券 `/user/coupons`.
- **积分商城:** 积分订单 `/user/integral_order`.
- **资产记录:** 平台余额 `/user/money`, 冻结资金 `/user/frozen_money`, 平台积分 `/user/integral`.
- **分销分佣:** 分销信息 `/user/distribution`, 分销会员 `/user/distribution_users`, 分销佣金 `/user/distribution_logs`.
- **帮助中心:** 网站公告、其他合作、帮助中心、关于我们 at `/user/article/:name`.
`users/default.vue` dashboard data shape: `user_info:{money,integral,frozen_money}`, `count[5]` for pending payment/shipping/receipt/comment/after-sale, `order[]` with `{id,created_at,order_no,order_status,order_status_cn,order_goods[]:{goods_image,goods_name,sku_name,buy_num,total_price}}`, `fav[]` with `{out_id,goods_master_image,goods_name,goods_price}`, and local `history[]` with `{id,image,goods_name,goods_price}`. It renders balance cards, five status links/badges, recent orders, recent favorites and browsing history, all in `.user_main` cards.
Focused page shapes:
- **Address:** `addresses[]:{id,receive_name,receive_tel,area_info,address,is_default}`; add/edit fields are `receive_name`, `receive_tel`, `area` (cascader), `address`, `is_default`; paginated response. Uses `el-dialog`, `el-form`, `el-pagination`.
- **Orders:** filter `{order_status,order_no,created_at:[start,end],is_type}`; statuses -1/0/1/2/3/4/5/6 = all/cancelled/wait-pay/wait-ship/wait-receive/wait-comment/after-sale/completed. Cards use the same order/item shape above and action buttons; logistics modal expects `order_info.delivery_list[]:{context,time}`.
- **Favorites:** toggle `isGoods`; product table fields `goods_master_image,goods_name,goods_price,created_at`, store table fields `store_logo,store_name,created_at`, records carry `is_type,out_id`.
- **Coupons:** generic table columns `name,money,use_money,end_time,created_at`; no custom card UI. `table-view` is a large Element-based shared component and should be replaced with a plain responsive table in Nuxt.
## 8. Stores, seckill, collective and integral pages
- **Stores list (`stores/list.vue`):** breadcrumb + one sort row (default/distance), then store rows with 80px logo and `{id,store_logo,store_name,store_company_name,area_info,store_address,distance}`; center hardcodes 好评率 100%, right shows distance and 进入店铺; 30/page pagination. Location comes from local `lonlat`. Replace `el-breadcrumb`, icons, image and pagination.
- **Store info (`stores/info.vue`):** 350px banner from `store_info.store_slide[]`, left store profile/rates/company/address/favorite/contact and sales ranking; right store-scoped sort (default/price/sales/comments) plus 220x300 product cards with `{id,goods_master_image,goods_name,goods_subname,goods_price,goods_market_price}` and pagination. Store payload adds `id,user_id,store_verify,store_status,store_logo,sale_list[]`, `rate:{scoreAll,agreeAll,serviceAll,speedAll}`, and `after_sale_service` where relevant.
- **Seckills (`seckills/index.vue`):** breadcrumb, four equal timed session selectors (`timeList` hours, `timeIndex`, `timeFormat` countdown), then a 4-column product grid using `{id,goods_master_image,goods_name,goods_subname,goods_price,goods_market_price}`, paginated by `{page,per_page,last_page,total,start_time}`. Countdown refreshes the page when the hour expires.
- **Collectives (`collectives/index.vue`):** 350px collective banner, then the same 3-row category/brand/sort filter and 5-column standard goods cards as `/s`; API query adds `is_collective:1`. Mock the same class/brand/filter/product/page shapes as goods list.
- **Integral home (`integral/index.vue`):** 350px banner, four info blocks (login/register or avatar + `userInfo.integral`, plus private customization/24H/priority cards), then “热门推荐” and category rails. Data is `{store_slide?,userInfo:{avatar,integral},recommend:[{id,goods_master_image,goods_name,goods_price,goods_market_price}],list:[{name,integral_goods:[same product shape]}]}`.
- **Integral goods/detail/order:** `integral/goods.vue` is a category/sort-only list (`class[]:{id,name}`, product card uses “立即兑换”); `integral/info.vue` is zoom gallery + points price/market price, sale/stock and quantity, “立即兑换”, with `goods_content`; `integral/orders/index.vue` reuses address/remark four-step checkout but has one points product `{goods_master_image,goods_name,sku_name,goods_price,buy_num}`, and submits `{id,buy_num,address_id,remark}` to points pay.
## 9. Auth pages
All three use `.home_login` with `user_login__bgs.png`, a 1200px `.login_block`, and right-floating white `.login_item` 400x450, 40px padding, 50px top margin. Inputs are 320px wide, 35px high; submit is full-width red `#ca151e`; no shared top/header is rendered inside the auth template (they still are routed beneath the Home shell in the current route tree).
- **Login:** title tabs “帐号登录 | 扫码登录”; `username`, `password`; 登录 button; links 注册 and 忘记密码; Element divider and WeChat icon/click. Mock `{username,password,provider:'users'}`.
- **Register:** title “注册账号 | 账号登录”; `username` (phone), `password`, `re_password`, SMS `code`, send-code countdown (`code_text`, `math`); 注册 + forgot-password link. Optional inviter id from query/session. Mock `{username,password,re_password,code,code_text,math}`.
- **Forgot password:** title “忘记密码 | 账号登录”; `username`, new `password`, `re_password`, SMS `code`, send-code state; 确定 + 立即登录. Same `{username,password,re_password,code,code_text,math}`.
Element replacements here: `el-divider` → border/label, `el-message` → toast, and ordinary inputs/buttons can be native/Tailwind.
## 10. Theme and visual language
- `plugins/css/style.css` resets body/common block margins and list styles, sets `html,body,#app` height 100%, base `font-size:14px`, color `#333`, no explicit `font-family` (browser/system font applies). Links default `#333` with no decoration; hover `#000`. `body` margin is zero.
- Fixed desktop container is `.center1200` and `.w1200`, both `width:1200px;margin:0 auto`; floats are `.float_left/.float_right`; `.clear` clears floats. Other recurring utility classes: `.breadcrumb` (30px vertical margin), `.shadow` (`0 2px 12px rgba(0,0,0,.1)`), `.x20` 1px gray divider with 20px margin, `.user_main` white padded card, `.table_lists` min-height 600px, `.block_title` 16px with 3px red left border.
- `base.scss` has `$admin_background:#f4f4f4`; `.home` Element primary CSS vars are `--el-color-primary:#e50e19` and light variants, but storefront source consistently uses `#ca151e` for active links, red prices, buttons, selected filters, nav, and borders. Preserve both: `#ca151e` is the visual brand red; `#e50e19` is Element theme fallback. Status colors are green `#67c23a`, gray `#909399`, yellow `#e6a23c`, red `#f56c6c` (round utility classes).
- Button language: white bordered `.default_btn`, red `.error_btn`, yellow `.warn_btn`, green `.success_btn`, gray `.gray_btn`; all compact 12px, 3px radius, 6px/15px padding. Cards/rows are mostly white with `#efefef` 1px rules, subtle hover lift/shadow, restrained 1216px text, and red price emphasis. Home/floor backgrounds use `#f5f5f5`; user center uses `#f1f1f1`; header nav is `#333` with a red category block.
- Replication recommendation: keep 1200px desktop grid, 2030px section rhythm, 20px card gaps, 40px header/nav spacing, 220224px product cards, gray separators, low-radius 3px controls, red active states and strikethrough market prices. Since the reference relies heavily on Element Plus (`el-carousel`, `el-image`, `el-breadcrumb`, `el-pagination`, `el-empty`, `el-checkbox`, `el-select`, `el-dialog`, `el-form`, `el-tabs`, `el-rate`, `el-result`, `el-badge`, `el-progress`, `el-timeline`, `el-icon`), a fresh Nuxt 3/Tailwind build should provide plain semantic replacements rather than porting Elements runtime.
@@ -0,0 +1,27 @@
# Proposal: mall-pc-storefront-replica
## Why
The current mall is a bare API-wired storefront (plain grid, minimal chrome). We want a real PC mall experience modeled on a classic open-source B2B2C buyer mall (Vue2 + element-plus, outdated): its layout, components and visual language — but rebuilt in our Nuxt 3 stack. The backend assortment/marketing/user-center endpoints for this experience do not exist yet, so the MVP serves everything from fixed mock data behind a mock API adapter, keeping the `$api` call shape so pages can later switch back to the live API without rewrites.
## What changes
- `apps/mall` is rebuilt as a full PC storefront replicating a classic B2B2C buyer mall: fixed top bar + header (logo/search/cart badge) + dark nav with category mega-menu + footer; 1200px container, `#ca151e` brand red, card/floor visual language (see `design-reference.md`).
- Mock layer: `apps/mall/mock/` provides (a) a `createMockApi()` adapter implementing the existing `@vmall/shared` API client surface with fixed data, selected via `runtimeConfig.public.mockApi` (default `true`), and (b) a mall-local mock content module for surfaces the real API has no counterpart for (banners, home floors, brands, seckill sessions, collectives, stores, comments, coupons, addresses, user dashboard stats).
- Pages (all mock-backed):
- Home `/`: hero carousel, quick-link/promo strip, category product floors.
- Goods list `/search`: breadcrumb, category/brand/sort filter block, 5-col product cards, pagination.
- Goods detail `/goods/[id]`: gallery + zoom, SKU/attr picker, quantity, price/market price, coupons/activity strip, store card + sales rail, tabs (detail/comments/after-sale).
- Cart `/cart`, checkout flow `/checkout``/checkout/pay``/checkout/success` with 4-step bar, address selection, mock payment.
- Auth: `/login`, `/register`, `/forgot-password` (right-floating white panel over banner background).
- User center `/user/**`: 234px left menu shell; dashboard, orders list/detail, addresses, favorites, coupons, invoices.
- Stores: `/stores` directory, `/stores/[id]` store home.
- Marketing: `/seckill` (timed sessions + countdown), `/collective`, `/integral` (points mall home).
- Element Plus widgets are NOT ported; plain Nuxt equivalents are built (carousel, pagination, tabs, checkbox, dialog, empty state, back-to-top, rating stars).
- All UI copy stays `$t()`-driven (en/zh via `locales-extra.ts`); mock content fields keep the `{"en","zh"}` JSONB shape; money stays integer minor units through `formatMoney`.
## Non-goals
- No backend changes; no changes to `@vmall/shared` contract, shop-admin, or admin apps.
- No SEO/SSR tuning, no real payment, no chat/customer-service, no distribution/cash/integral-order subsystems beyond display mocks.
- No element-plus or Tailwind introduction; styling is scoped CSS in the mall app.
## Capabilities
- `frontend-mall`: rebuilt PC storefront experience (mock-backed MVP).
@@ -0,0 +1,75 @@
# Spec delta: frontend-mall
## ADDED Requirements
### Requirement: B2B2C mall-style PC storefront shell
The mall SHALL render a buyer-facing desktop shell modeled on a classic B2B2C PC mall: a 30px utility bar, logo/search/cart header, dark primary navigation with a hover category mega-menu, a 1200px content grid, and a value-proposition footer. The visual language SHALL use #ca151e for brand/price/active states, #f5f5f5 section backgrounds, gray hairline borders, compact controls, and product-card hover lift/shadow. The implementation SHALL use Nuxt-native semantic components and SHALL NOT depend on Element Plus.
#### Scenario: shopper opens any mall page
- **WHEN** a shopper navigates to a buyer-facing route
- **THEN** the shared desktop shell wraps the route content and its nav/search/cart controls are usable
### Requirement: Mock API adapter
The mall SHALL default to a fixed-data mock adapter that implements the existing `@vmall/shared` API client surface. The adapter SHALL support auth, catalog, currency conversion, cart mutation, checkout, order/payment/shipment state, and invoices without a backend. A runtime configuration switch SHALL select the live API client later without changing page call sites.
#### Scenario: mall runs without backend
- **WHEN** the mall starts with mock mode enabled and the API service is unavailable
- **THEN** browsing, cart, checkout, payment, orders and invoice pages return deterministic mock data and remain functional
### Requirement: Mock PC home page
The mall home page SHALL render a hero carousel, a six-item quick-link strip with promotion tiles, and bilingual product floors with a left advert and product-card grid.
#### Scenario: shopper lands on home
- **WHEN** `/` loads in mock mode
- **THEN** the carousel, quick links, promotions and every non-empty product floor render fixed mock content
### Requirement: Product discovery pages
The mall SHALL provide `/search` with breadcrumb, category, brand and sort controls, a five-column desktop product grid, pagination and an empty state. It SHALL provide `/goods/[id]` with image gallery/zoom, bilingual name/subtitle, integer-minor-unit prices, attribute and SKU selection, stock-aware quantity, coupons/activity summary, store card, sales rail, and detail/comments/after-sale tabs.
#### Scenario: filter and inspect a product
- **WHEN** a shopper filters the search page and opens a product
- **THEN** matching mock cards are shown and selecting an in-stock SKU updates the displayed price, stock and cart target
### Requirement: Mock transaction flow
The mall SHALL provide a store-grouped cart, address-selecting checkout preview, mock payment selection and payment-success result. Cart quantity/removal, selection totals, checkout and mock payment SHALL update the in-memory mock state for the current browser session.
#### Scenario: complete mock purchase
- **WHEN** a shopper adds an in-stock SKU, checks out with a mock address and confirms a mock payment
- **THEN** the cart is cleared, the success page is shown and the new order appears in the user order list
### Requirement: Auth and buyer center
The mall SHALL provide B2B2C mall-style login, register and forgot-password panels backed by deterministic mock auth. `/user` SHALL render a two-column buyer center with dashboard, order list/detail, addresses, favorites, coupons and invoices.
#### Scenario: sign in and inspect buyer data
- **WHEN** a shopper submits the mock login form and opens `/user`
- **THEN** the buyer-center shell and fixed account/order/address/favorite/coupon/invoice data render without backend access
### Requirement: Store and marketing pages
The mall SHALL provide a store directory, store home, timed seckill page, collective-buy list and points-mall home using fixed bilingual mock content. Marketing pages MAY be display-only except navigation to product detail.
#### Scenario: navigate storefront discovery channels
- **WHEN** a shopper opens stores, seckill, collective or integral routes
- **THEN** each page renders the appropriate B2B2C mall-style banner/filter/session/card layout and product links resolve to mock product details
## MODIFIED Requirements
### Requirement: Localized storefront
The mall SHALL render every UI string and all catalog/store/marketing mock content in en or zh from one switcher, defaulting to en. Switching locale SHALL update the desktop shell and current page without a full reload.
#### Scenario: switch to Chinese
- **WHEN** a shopper switches locale to zh
- **THEN** navigation, buttons, product/store names and mock marketing content render in Chinese without reload errors
### Requirement: Multi-currency display
The mall SHALL offer a currency switcher sourced from the selected API adapter and SHALL convert product, cart and order prices through integer minor units and each currency's exponent. Mock mode SHALL use deterministic fixed conversion rates; live mode SHALL use the API.
#### Scenario: switch currency
- **WHEN** a shopper switches from USD to JPY on a product priced $10.00
- **THEN** the displayed price reflects the selected adapter's conversion rate with JPY exponent 0 and no floating-point money arithmetic
### Requirement: Shopping flow
A shopper SHALL be able to browse, view detail, add to cart, checkout with a shipping address, pay (mock), track orders/shipments, confirm delivery, and request an invoice through the selected API adapter. The MVP SHALL default to the fixed-data mock adapter and SHALL not require the live API.
#### Scenario: end-to-end purchase
- **WHEN** a shopper completes checkout on a non-empty mock cart
- **THEN** the resulting order appears in the buyer center and the cart is empty
@@ -0,0 +1,34 @@
# Tasks: mall-pc-storefront-replica
## 1. Mock layer
- [x] `apps/mall/mock/data.ts`: fixed bilingual catalog (categories tree, brands, products+SKUs+images, stores, banners, home floors, seckill sessions, collectives, integral goods, comments, coupons, addresses, user profile/orders/invoices), money in minor units
- [x] `apps/mall/mock/api.ts`: `createMockApi()` implementing the `@vmall/shared` client surface (auth, products, categories, currencies+convert, cart with in-memory state, checkout, orders, shipments, invoices) returning fixed/derived data
- [x] `apps/mall/plugins/api.ts`: select mock vs real client via `runtimeConfig.public.mockApi` (default `true`)
## 2. Shell + shared components
- [x] Layout: `TopBar` (30px, city/links/login state), `SiteHeader` (logo, search box, cart badge, dark nav + category mega-menu `CategoryMenu`), `SiteFooter` (value props + copyright), scroll-aware header collapse, back-to-top
- [x] Primitives: `ProductCard`, `Pagination`, `Breadcrumb`, `QtyStepper`, `RatingStars`, `EmptyState`, `Modal`, `Tabs`, `Carousel`, `StepBar`; classic B2B2C mall theme via scoped CSS (1200px container, #ca151e, #f5f5f5 floors, card hover lift)
- [x] i18n: all new UI strings in `apps/mall/locales-extra.ts` (en/zh)
## 3. Browse pages
- [x] Home `/`: hero carousel, quick-link/promo strip, category floors (left adv + product cards)
- [x] Goods list `/search`: category/brand/sort filter block, 5-col cards, pagination, empty state
- [x] Goods detail `/goods/[id]`: gallery+zoom, attr/SKU picker, qty stepper, price block, coupon/activity strip, store card + sales rail, detail/comments/after-sale tabs
## 4. Transaction pages (mock)
- [x] Cart `/cart`: store-grouped table, select/qty/remove, summary + 结算
- [x] Checkout `/checkout`: step bar, address select, store-grouped preview, remark, submit
- [x] Pay `/checkout/pay` + success `/checkout/success`
## 5. Auth + user center
- [x] `/login`, `/register`, `/forgot-password` panels
- [x] `/user` shell: left menu; dashboard, orders list + detail, addresses, favorites, coupons, invoices
## 6. Stores + marketing pages
- [x] `/stores` directory + `/stores/[id]` store home
- [x] `/seckill` sessions + countdown grid, `/collective` list, `/integral` points mall home
## 7. Verify
- [x] `pnpm --filter @vmall/mall build` green
- [x] `openspec change validate mall-pc-storefront-replica --strict` green
- [x] Browser smoke: home, search, goods detail (SKU pick), cart → checkout → pay → success, login, user center, stores, seckill — in both locales