docs(openspec): propose 8 tigshop migration changes (P0-P7) + migration plan

This commit is contained in:
Chengdong Zhang
2026-09-23 13:30:36 +08:00
parent 0d0e10b97b
commit 9a749e2551
47 changed files with 1607 additions and 0 deletions
@@ -0,0 +1,27 @@
## Why
The mall top bar's "Mobile" (手机端) entry is a dead link to `/` and no mobile client exists at all: the entire mobile journey — browse, buy, track orders, after-sale, coupons, wallet — is missing. tigshop's uniapp client cannot be adopted because its `wx.*` mini-program capabilities have no portable equivalent, so only its information architecture is worth migrating, rebuilt as a first-party web client.
## What Changes
- Add `apps/mobile`, a standalone Nuxt3 H5 app served on port 3003 that reuses `@vmall/shared` contracts and `theme.css`. No backend capability changes: every surface consumes existing APIs through the shared client; any API gap is recorded as a dependency, not implemented here.
- Ship the migrated information architecture: home content floors, category browse, search, product detail, cart, checkout, order list/detail, after-sale entry (reusing the `aftersale` API from `add-aftersale-refunds`), coupon center, wallet (reusing the `wallet` API from `add-wallet-settlement`), "My" center, sign-in and register.
- Wrap the app in a custom bottom tab bar (Home / Category / Cart / My) with safe-area adaptation and mobile interaction conventions (pull-to-refresh, sign-in redirect that returns to the origin).
- Keep the session contract identical to the mall: same `vmall.token`/`vmall.user` storage, Bearer header, and `me()` validation semantics.
- Point the mall top-bar "Mobile" entry at the H5 app instead of the dead `/` link.
## Capabilities
### New Capabilities
- `frontend-mobile`: Standalone Nuxt3 H5 storefront — page inventory and navigation structure, pure consumption of `@vmall/shared` (no app-local API wrapper), mobile shell/interaction conventions (tab bar, safe area, pull-to-refresh), and a token contract identical to the mall's.
### Modified Capabilities
None. Backend capabilities are reused as-is (after-sale and wallet surfaces consume `aftersale` and `wallet` from `add-aftersale-refunds` and `add-wallet-settlement`), and `frontend-mall` behavior changes only in the single top-bar link target.
## Non-goals
Mini-program and native APP builds, WeChat login/payment/sharing/subscription messages, IM customer service, PWA offline mode, push notifications, and a responsive retrofit of `apps/mall` are all excluded. Mobile-optimizing `admin` or `shop-admin` is excluded. Dedicated address-book management, reviews/ratings, and any new backend endpoint are excluded.
## Impact
Adds one new pnpm workspace app (`apps/mobile`, Nuxt3, port 3003) consuming `@vmall/shared` and `theme.css`, plus a one-line link fix in the mall top bar. Deliberately no migrations, no Rust modules, and no shared-contract changes; after-sale and wallet pages depend on `add-aftersale-refunds` and `add-wallet-settlement` being archived and degrade to localized "Coming soon" placeholders until then.
@@ -0,0 +1,85 @@
## ADDED Requirements
### Requirement: Mobile page inventory and navigation structure
The `apps/mobile` H5 app SHALL provide the migrated tigshop information architecture as Nuxt routes: home `/`, category browse `/category`, search `/search` with results `/search/result`, product detail `/product/:id`, cart `/cart`, checkout `/checkout`, order list `/orders`, order detail `/orders/:id`, coupon center `/coupon`, wallet `/wallet`, "My" center `/user`, and sign-in `/login` with register `/register`. Every listed route SHALL be reachable through navigation from the tab bar, the "My" center, or in-flow links, and no route SHALL depend on mini-program-only capabilities.
#### Scenario: walk the browse-to-buy path
- **WHEN** a mobile shopper opens `/`, drills into `/category`, opens `/search/result`, selects a product, adds it to the cart, checks out, and then opens `/orders/:id`
- **THEN** each route renders its documented surface from the shared API client and the created order is visible in the order list and detail
#### Scenario: user-center entries resolve
- **WHEN** a shopper opens `/user` and taps the coupon, wallet, order, or after-sale entry
- **THEN** each entry navigates to its documented route and no entry points at an unbuilt placeholder route
### Requirement: Custom tab bar and safe-area shell
The app SHALL render a custom fixed bottom tab bar with exactly four destinations — Home, Category, Cart, My (首页/分类/购物车/我的) mapped to `/`, `/category`, `/cart`, `/user` — with the active tab indicated by the shared brand token. Tab bar and page chrome SHALL pad with `env(safe-area-inset-bottom)` and `env(safe-area-inset-top)` so content is never hidden behind the home indicator or status bar. Routes outside the four tab routes SHALL replace the tab bar with a top back-bar instead.
#### Scenario: tab bar switches root sections
- **WHEN** a shopper taps a tab bar item
- **THEN** the matching root route renders with that tab shown active and the tab bar stays fixed above the safe area
#### Scenario: content clears the home indicator
- **WHEN** a shopper opens the cart on a device with a non-zero bottom safe-area inset
- **THEN** the last cart line and the tab bar remain fully visible above the inset
### Requirement: Shared-contract-only data access
The app SHALL obtain every piece of backend data through the `@vmall/shared` API client and its existing adapter selection (live backend or fixed-data fallback) and SHALL NOT define its own fetch wrapper, endpoint paths, or response types. Styling SHALL consume `@vmall/shared/theme.css` tokens rather than a private global stylesheet. Money SHALL be handled as integer minor units with shared formatting helpers and bilingual content SHALL render the active locale from `{en, zh}` localized text.
#### Scenario: no private API layer
- **WHEN** a developer inspects `apps/mobile`
- **THEN** all backend calls go through `@vmall/shared` client methods and no page defines its own HTTP request to an API path
#### Scenario: prices render from minor units
- **WHEN** any surface renders a price
- **THEN** the amount derives from integer minor units through shared formatting with no floating-point money arithmetic
### Requirement: Mobile interaction conventions
Home, search results, and order list SHALL support pull-to-refresh reloading through the shared client. Actions requiring authentication SHALL redirect a signed-out shopper to `/login` and return them to the interrupted route after sign-in. In-flight mutations (add to cart, place order, claim coupon) SHALL be guarded against duplicate submission.
#### Scenario: pull to refresh home
- **WHEN** a shopper pulls down on the home page
- **THEN** home content floors reload from the shared client and the refreshed state renders
#### Scenario: anonymous add returns to the product
- **WHEN** a signed-out shopper adds a product to the cart from `/product/:id` and then signs in
- **THEN** they land back on that product route
### Requirement: Mall-identical session and token contract
The app SHALL persist the session exactly as the mall does — `AuthTokens.token` in `localStorage["vmall.token"]` and the user in `localStorage["vmall.user"]` — and SHALL send it as a Bearer header via the shared client's `getToken`. A restored token SHALL be validated against the auth API on load and a rejected token SHALL clear the stored session and route the shopper to `/login`, matching mall semantics so one sign-in serves both apps on the same origin storage.
#### Scenario: token validated on load
- **WHEN** a shopper with a stored token reopens the app
- **THEN** the session is validated through the shared auth API and the "My" center renders the authenticated user
#### Scenario: rejected token clears storage
- **WHEN** a restored token is rejected by the auth API
- **THEN** `vmall.token` and `vmall.user` are cleared and the shopper is routed to `/login`
### Requirement: Transaction and account surfaces on existing APIs
Cart, checkout, order list and order detail SHALL read and write backend state exclusively through shared client cart, checkout, order and address methods, so carts and orders survive reloads and match the mall's data. Checkout SHALL offer the customer's saved addresses (defaulting to the default address) with an inline manual address form when none exist, and SHALL present eligible owned coupons per shop order. Order detail SHALL expose the appropriate status actions (pay, cancel, confirm delivery) through the shared client's guarded status transitions. Coupon center SHALL list claimable shop coupons, claim them, and show the customer's owned coupons through the shared client.
#### Scenario: cart survives reload
- **WHEN** a signed-in shopper adds an item and reloads `/cart`
- **THEN** the line is still present from backend state
#### Scenario: claim from the coupon center
- **WHEN** a signed-in shopper claims a coupon with remaining stock from `/coupon`
- **THEN** it appears in the owned-coupon list without any fixture import
### Requirement: Dependency-gated after-sale and wallet surfaces
The after-sale entry SHALL expose the customer's after-sale requests through the `aftersale` shared-client API from `add-aftersale-refunds`, and the wallet page SHALL render the customer's balances and ledger entries through the `wallet` shared-client API from `add-wallet-settlement`. While either change is not yet archived, the corresponding surface SHALL render a localized "Coming soon" placeholder instead of broken or fixture-backed behavior, and SHALL be enabled without further design change once its dependency lands.
#### Scenario: wallet before its dependency lands
- **WHEN** `add-wallet-settlement` is not yet archived and a shopper opens `/wallet`
- **THEN** a localized "Coming soon" placeholder renders instead of an error
#### Scenario: after-sale after its dependency lands
- **WHEN** `add-aftersale-refunds` is archived and a shopper opens the after-sale entry from `/orders/:id`
- **THEN** after-sale data renders from the `aftersale` shared-client API
### Requirement: Reachable mobile entry from the mall shell
The mall top bar's "Mobile" (手机端) entry SHALL resolve to the H5 app's home page instead of the current dead `/` link, so the desktop storefront is the discoverable entry point for the mobile client.
#### Scenario: top-bar mobile link opens the H5 app
- **WHEN** a desktop shopper clicks the mall top bar's Mobile entry
- **THEN** the `apps/mobile` home page opens instead of reloading the mall home
+36
View File
@@ -0,0 +1,36 @@
## 1. Scaffold and workspace wiring
- [ ] 1.1 Scaffold `apps/mobile` as a Nuxt3 app `@vmall/mobile` picked up by the existing `apps/*` pnpm workspace glob, with `dev`/`build`/`preview` scripts, dev server port 3003 (mall 3000, shop-admin 3001, admin 3002), and en/zh i18n matching the mall's locale source.
- [ ] 1.2 Import `@vmall/shared/theme.css` and style exclusively with shared tokens and Tailwind utilities (brand `#ca151e`, `#f5f5f5` surfaces); no private global stylesheet.
- [ ] 1.3 Wire `@vmall/shared` `createApi` as the only data path — mall-consistent `runtimeConfig.public.apiBase`, `getToken` reading `localStorage["vmall.token"]`, and the same per-domain live/fixed adapter selection as the mall (reuse the existing fixed adapter instead of authoring new API glue). No app-local fetch wrapper, endpoint paths, or response types.
- [ ] 1.4 Implement the session store on the mall token contract: `AuthTokens.token` in `localStorage["vmall.token"]`, user in `localStorage["vmall.user"]`, `me()` validation on load, and clearing plus `/login` redirect when the token is rejected.
## 2. Shell, tab bar, home, category, search
- [ ] 2.1 Build the custom fixed bottom tab bar with exactly four tabs — Home `/`, Category `/category`, Cart `/cart`, My `/user` (首页/分类/购物车/我的) — active-tab styling on shared brand tokens, `env(safe-area-inset-bottom)` padding plus matching content padding, `env(safe-area-inset-top)` on page chrome, and a top back-bar replacing the tab bar on non-tab routes.
- [ ] 2.2 Build home `/` rendering `getHomeContent()` banners, promotions, quick links and bilingual product floors in stacked mobile layout, with pull-to-refresh reload and integer-minor-unit prices through shared format helpers.
- [ ] 2.3 Build category `/category`: two-panel browse (top-level rail plus children) from `listCategories()`, drilling into `/search/result` scoped to the selected category subtree.
- [ ] 2.4 Build search `/search` and `/search/result`: keyword input, category/brand/sort filters, paginated product grid from `listProducts()`, empty state, pull-to-refresh.
- [ ] 2.5 Point the mall top bar "Mobile"/"手机端" entry (currently a dead `NuxtLink to="/"`) at the `apps/mobile` home URL; leave the rest of the desktop shell unchanged.
## 3. Product detail, cart, checkout
- [ ] 3.1 Build `/product/:id`: image gallery, bilingual name/subtitle, attribute and SKU selection with stock-aware quantity, integer-minor-unit prices, shop card, and add-to-cart via `addCartItem` with in-flight duplicate-tap guarding and anonymous → `/login` redirect returning to the product route.
- [ ] 3.2 Build `/cart`: shop-grouped live cart via `getCart`/`updateCartItem`/`removeCartItem`, minor-unit totals, persistence across reload.
- [ ] 3.3 Build `/checkout`: saved-address selection defaulting to the default address with inline manual address form when none exist, per-shop owned coupon selection via `listMyCoupons`, submission via `checkout(...)` (money as integer minor units throughout), payment through the shared pay contract, and success routing to the new `/orders/:id`.
## 4. User center, orders, after-sale entry, coupons, wallet
- [ ] 4.1 Build `/user` "My": `getAccountSummary` balances/points, and an entry grid to orders, coupons, wallet, and after-sale, plus sign-in/register links when signed out.
- [ ] 4.2 Build `/orders` (paginated `listMyOrders`, pull-to-refresh) and `/orders/:id` (`getOrder`) with status-appropriate actions through the shared client's guarded transitions: `payOrder`, `cancelOrder`, `confirmDelivered`.
- [ ] 4.3 Build the after-sale entry from `/orders/:id` and `/user` backed by the `aftersale` shared-client API from `add-aftersale-refunds` (list + request detail). Dependency: if `add-aftersale-refunds` is not yet archived when this task is implemented, render a localized "Coming soon" placeholder and keep this task unchecked with a blocking note — no fixtures or invented endpoints.
- [ ] 4.4 Build `/coupon` coupon center: claimable shop coupon templates and claiming via `listShopCouponTemplates`/`claimCoupon`, owned coupons via `listMyCoupons`, anonymous claim redirecting to `/login` and returning to `/coupon`.
- [ ] 4.5 Build `/wallet` on the `wallet` shared-client API from `add-wallet-settlement` (balances and ledger entries). Dependency: if `add-wallet-settlement` is not yet archived when this task is implemented, render a localized "Coming soon" placeholder and keep this task unchecked with a blocking note.
- [ ] 4.6 Build `/login` and `/register` on the shared auth client with mall-identical semantics: minimum 8-character registration password validated before the API call, distinct invalid-credentials vs already-registered copy, and return-to-origin routing.
## 5. Verification and OpenSpec cleanup
- [ ] 5.1 Add focused integration coverage in `apps/api/tests/`, reusing the `tests/common/mod.rs` fixtures, for the existing endpoints the H5 flows consume (home content, cart → checkout → order, coupon claim, order listing/transition guards) so the mobile contract stays green; no new backend behavior is introduced by this change.
- [ ] 5.2 Build the affected frontends: `pnpm --filter @vmall/mobile build` and `pnpm --filter @vmall/mall build` (top-bar link change).
- [ ] 5.3 Run the API plus `apps/mobile` and browser-smoke at a mobile viewport (e.g. 390×844): tab bar navigation and safe-area padding, home floors, category → search → product → add to cart → checkout → order detail, coupon claim, pull-to-refresh, and anonymous sign-in return routing.
- [ ] 5.4 Check every task in this file, then run `openspec change validate add-mobile-h5 --strict` and `openspec validate --all --strict` and fix anything until both pass.