3.4 KiB
3.4 KiB
Task: build apps/shop-admin (merchant console) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/shop-admin, port 3001. Allowed roles: shop_owner, shop_staff.
Replace the placeholder pages. app.vue (sidebar layout) exists — keep it.
Pages to implement (apps/shop-admin/pages/)
login.vue— per conventions; only shop_owner/shop_staff may proceed.index.vue(auth) — dashboard: stat cards (total products, published count, orders by status counts, pending invoice requests) computed from$api.shop.listMyProducts({ per_page: 100 }),$api.shop.listOrders({ per_page: 100 }),$api.shop.listInvoices(). Also show shop profile from$api.shop.getMyShop()(localized name, slug, status).products/index.vue(auth) — table: localized name, slug, SKU count, status badge, created; status filter select (all/draft/published/unpublished); actions: edit link, publish ($api.shop.publish) / unpublish ($api.shop.unpublish) buttons with immediate refetch; "New product" button → products/new. Paging if total > per_page.products/new.vue+products/[id].vue(auth) — product form (shared componentcomponents/ProductForm.vue): slug, name EN + ZH, description EN + ZH (textareas), category select ($api.listCategories(), optional), images textarea (one URL per line → string[]). Save: create →$api.shop.createProduct, edit →$api.shop.updateProduct; body type ProductUpsertBody. On the EDIT page additionally: SKU manager — table of existing SKUs (sku_code, price formatted, currency, stock, active) + form to add/update a SKU ($api.shop.upsertSku(productId, body)with SkuUpsertBody): sku_code, price in MAJOR units converted to minor ×100 (currencies USD/CNY/EUR all exponent 2; offer a select of those three), stock integer, active checkbox. After upsert, refetch the product via$api.shop.getProduct(id)(exists in the client). Validation: slug required (lowercase alnum + dash), name EN required; show API errors (e.g. publish without priced SKU → 400) via .error-text.orders/index.vue(auth) —$api.shop.listOrders({ status, page }): status filter, table (order_no, created, items count, total in order.currency formatted, status badge), row → orders/[id].orders/[id].vue(auth) — order items, address, status. Create-shipment card (when status paid or fulfilling): carrier + tracking_no inputs + one qty input per order item (default = unshipped remainder = item.qty − sum of that item's qty across this order's shipments from$api.shop.listShipments()filtered by order_id); submit →$api.shop.createShipment(orderId, carrier, trackingNo, items)with only qty>0 lines. Shipments of this order: table with status + "Mark shipped" button when pending ($api.shop.markShipped). Refetch after every action.shipments.vue(auth) —$api.shop.listShipments(): full table (shipment_no, order_no, carrier, tracking_no, status badge, created) + mark-shipped action.invoices.vue(auth) —$api.shop.listInvoices(): table (invoice_no or —, order_no, title, kind, tax_no, amount formatted, status badge) + "Issue" button when requested ($api.shop.issueInvoice).
Middleware
middleware/auth.ts per conventions; apply to everything except /login.
Verify
pnpm --filter @vmall/shop-admin build MUST pass. Fix type errors properly (no any, no @ts-ignore).
Report back
Pages built, deviations, contract gaps found.