3.5 KiB
3.5 KiB
Task: build apps/mall (customer storefront) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/mall, port 3000. Allowed roles: customer.
Replace the placeholder pages with the real storefront. app.vue (topnav with locale + currency switchers) already exists — keep it; you may refine but not regress it. Currency selection comes from usePrefs().currency (cookie) and is fed by the switcher in app.vue.
Pages to implement (apps/mall/pages/)
index.vue— catalog home. Category filter (select, from$api.listCategories()), search input (q), paging (prev/next). Product grid (.grid.products): card with first image (or placeholder div), localized name viat(name, locale), price = lowest active-SKU price, displayed in the SELECTED currency. Price display helper (makecomposables/usePrice.ts): given amount_minor + sku currency, if it equals selected currency use as-is, else$api.convert(...); cache results in a reactive Map keyedamount:from:toto avoid duplicate calls; format withformatMoney(converted, selected, exponentOfSelected, locale). Load currency list once (store it in the composable via useState) for exponents.products/[id].vue— detail.$api.getProduct(route.params.id): images (main + thumbs), localized name/description, SKU picker (radio list: sku_code + attributes JSON rendered as key: value pairs + stock), qty input (1..stock), converted price, add-to-cart button. Not logged in → redirect /login (preserve intent not required). Success → navigate to /cart.cart.vue(auth) —$api.getCart(): line items (localized name, sku_code, unit price converted, qty editor with update on change$api.updateCartItem, remove$api.removeCartItem), line totals, subtotal (sum of converted lines), checkout button → /checkout. Empty state.checkout.vue(auth) — shipping address form (recipient/phone/country/region/city/line1/postal_code, all validated non-empty except region/postal optional-but-shown), order preview (lines + subtotal in selected currency), place order →$api.checkout(address, currency)→ success page state: show created order numbers + link to /orders.orders/index.vue(auth) —$api.listMyOrders(page): table (order_no, created, item count, total formatted in order.currency, status badge), link to detail. Paging.orders/[id].vue(auth) —$api.getOrder(id): items table, address card, status badge, total. Actions by status: pending_payment → Pay ($api.payOrder) and Cancel ($api.cancelOrder). Shipments section: from$api.listMyShipments()filtered by order_id — carrier/tracking/status, confirm-delivery button when shipped ($api.confirmDelivered). Invoice section: request form (kind select personal/company, title, tax_no shown when company) →$api.requestInvoice; show existing invoice for this order from$api.listMyInvoices().invoices.vue(auth) —$api.listMyInvoices(): table (invoice_no or —, order_no, title, kind, amount formatted, status badge, issued_at).login.vue,register.vue— .form-narrow cards. Register:$api.register(email, password, displayName)then setAuth and go home. Link between the two.
Middleware
middleware/auth.ts per conventions; apply to cart, checkout, orders/*, invoices.
Verify
pnpm --filter @vmall/mall build MUST pass. Fix all type errors properly (no any, no @ts-ignore).
Report back
Pages built, any deviations, anything missing from the API client contract (e.g. a pay endpoint) — list precisely.