Files
vmall/docs/frontend-guidelines.md
T

3.0 KiB

Frontend Guidelines (mall / shop-admin / admin)

Three Nuxt 3 apps, one design system, one API contract. Ports are fixed: mall 3000, shop-admin 3001, admin 3002.

Hard rules

  • Contract only. All API access goes through $api (the ApiClient from @vmall/shared). Never build URLs or fetch manually. Contract changes happen in packages/shared/src/types.ts + api.ts and must keep all three apps building.
  • No any / as any / @ts-ignore. Guard external data with types.
  • Money. formatMoney(minor, code, exponent, locale) or the mall's PriceText component. Never float math, never hardcode exponent 2.
  • i18n. All copy via $t(). Missing keys go to the app's own locales-extra.ts (mall: locales/*.ts modules), never to the shared packs from an app change. Keep en/zh key sets identical.
  • Layout. Tailwind v4 utilities with theme tokens (bg-bg, text-text, border-border, bg-surface, text-primary, text-danger, text-success, text-muted). No <style scoped>. Prefer @vmall/ui components (VBtn, VCard, VField, VInput, VTable, VPage, VPanel, VBadge) over raw elements.

Page anatomy (follow it)

<script setup lang="ts">
definePageMeta({ middleware: "auth" });       // protected pages
const { $api } = useNuxtApp();
const loading = ref(true);                    // always handle loading
const errorMessage = ref("");                 // visible failures, role="alert"
// load in onMounted (session lives in localStorage; SSR renders loading state)
</script>
<template>
  <VPage :title="$t('nav.x')">…</VPage>
</template>

Patterns proven in the codebase: list page + inline row editing (apps/admin/pages/currencies.vue), ordered whole-list replacement editors (apps/admin/pages/content.vue, brands.vue), state-machine action pages with 409 surfacing (apps/shop-admin/pages/aftersales/).

The mall mock boundary

apps/mall/plugins/api.ts composes createMockApi() with per-domain live picks. Adding a mall-facing domain means:

  1. implement the methods in apps/mall/mock/api.ts (deterministic, mutable, persisted via the STORAGE_KEY versioned localStorage state),
  2. add the domain to LiveDomain + LIVE_PICKS with exact method picks (a missed method silently falls back to the mock while the domain looks live),
  3. enable it in nuxt.config.ts liveDomains and in mock seeds (apps/mall/mock/data.ts) when new required model fields appear.

NUXT_PUBLIC_LIVE_DOMAINS is a JSON array env var (e.g. '["catalog"]'), not a comma string.

Dev-server pitfalls (learned the hard way)

  • After pnpm build, dev servers need rm -rf apps/<app>/.nuxt or Vite fails with #app-manifest pre-transform errors.
  • Dev servers bind IPv6 localhost only; probe localhost, not 127.0.0.1.
  • Long-lived browser tabs across dev-server restarts lose hydration (stale Vite chunk hashes). When a page sticks on "Loading…" with no console errors, restart the dev server and open a fresh tab.