Files
vmall/openspec/changes/archive/2026-09-17-frontend-apps/agent-conventions.md
T

3.2 KiB

VMall frontend conventions (READ FIRST — applies to every app agent)

Repo: /Users/chengdzhang/github/jamyun/vmall (pnpm workspace). The Rust API is COMPLETE and tested; do not touch apps/api, packages/shared, openspec, or any app other than yours.

Your app

  • Nuxt 3 + pinia + @nuxtjs/i18n. Deps installed. Verify with pnpm --filter @vmall/<your-app> build at the end (MUST pass). Do not run dev servers, do not run other apps' builds, do not run cargo.
  • The API may not be running while you work — code against the contract, prove with nuxt build (type safety + compile). NEVER mock the API.

Contract (packages/shared/src)

  • useNuxtApp().$api is a typed ApiClient (see packages/shared/src/api.ts for every method + request body types). Provided by plugins/api.ts (already wired, reads baseUrl from runtimeConfig public.apiBase, token from localStorage vmall.token).
  • Types in packages/shared/src/types.ts (User, Product, Sku, Cart, Order, Shipment, Invoice, Currency, Paged, etc.). Money = integer minor units + ISO currency code.
  • t(localizedText, locale) picks the display string from a JSONB {"en","zh"} map. formatMoney(amountMinor, currency, exponent, locale) formats minor units.
  • All UI strings via $t with keys from @vmall/shared/locales (en + zh exist). If you need a key that doesn't exist, add it to YOUR app's locales-extra.ts (enExtra/zhExtra, same nested shape) — NEVER edit packages/shared.
  • Shared stylesheet @vmall/shared/ui.css is loaded: use its classes (.card, .btn .primary .sm .danger, .table, .badge .green/.blue/.orange/.red, .field, .grid.products, .product-card, .page-title, .page-head, .form-narrow, .muted, .row, .between, .mt, .mb, .error-text). Add app-scoped CSS only in a <style> block when needed.

Session & guards

  • stores/session.ts: useSessionStore() — hydrate() on mounted (app.vue already does), setAuth({token,user}), logout(), getters.isLoggedIn, state.user (role, display_name).
  • Route guard convention: create middleware/auth.ts with defineNuxtRouteMiddleware that (client-side) reads localStorage vmall.token + vmall.user (JSON) and redirects to /login when absent or when user.role is not in the app's allowed roles. Apply with definePageMeta({ middleware: "auth" }) on every protected page. The /login page itself is public.
  • Login page pattern: form → $api.login(email, password) → on success check user.role against the app's allowed roles (wrong role → show error, do not setAuth) → session.setAuth(...) → navigateTo("/"). Show API error message on failure.

Behavior rules

  • All pages render in BOTH en and zh — switcher already in app.vue; never hardcode user-facing strings.
  • All list pages handle empty state ($t('common.empty')) and API errors (show err.message in .error-text).
  • After every mutation, re-fetch the affected list/detail from the API (no local-only state faking).
  • Dates: new Date(x).toLocaleString(locale === 'zh' ? 'zh-CN' : 'en-US').
  • Money inputs: enter major units (e.g. 12.99), convert to minor via exponent (SKU currencies are exp-2 except JPY exp-0; SKU editor may restrict currency to USD/CNY/EUR which are all exp-2).
  • Status badges: map statuses to .badge colors (green=positive terminal, blue=active/in-progress, orange=pending, red=cancelled/rejected).