From 653f13161acbff3d97e2dea93a6a7ff417f9abc7 Mon Sep 17 00:00:00 2001 From: Chengdong Zhang Date: Thu, 17 Sep 2026 13:34:38 +0800 Subject: [PATCH] feat: three nuxt frontends, demo seed, rounding + money-exponent + rate-cast fixes, archived specs --- apps/admin/i18n.config.ts | 20 +- apps/admin/locales-extra.ts | 67 +++++ apps/admin/middleware/auth.ts | 11 + apps/admin/nuxt.config.ts | 2 +- apps/admin/pages/currencies.vue | 238 +++++++++++++++++ apps/admin/pages/index.vue | 158 ++++++++++- apps/admin/pages/login.vue | 90 +++++++ apps/admin/pages/orders.vue | 128 +++++++++ apps/admin/pages/shops.vue | 152 +++++++++++ apps/admin/pages/users.vue | 193 ++++++++++++++ apps/api/src/money.rs | 5 +- apps/api/tests/catalog.rs | 9 + apps/mall/components/PriceText.vue | 32 +++ apps/mall/components/StatusBadge.vue | 19 ++ apps/mall/composables/usePrice.ts | 60 +++++ apps/mall/i18n.config.ts | 20 +- apps/mall/locales-extra.ts | 111 ++++++++ apps/mall/middleware/auth.ts | 15 ++ apps/mall/nuxt.config.ts | 3 +- apps/mall/pages/cart.vue | 134 ++++++++++ apps/mall/pages/checkout.vue | 136 ++++++++++ apps/mall/pages/index.vue | 146 ++++++++++- apps/mall/pages/invoices.vue | 65 +++++ apps/mall/pages/login.vue | 39 +++ apps/mall/pages/orders/[id].vue | 197 ++++++++++++++ apps/mall/pages/orders/index.vue | 78 ++++++ apps/mall/pages/products/[id].vue | 143 ++++++++++ apps/mall/pages/register.vue | 41 +++ apps/shop-admin/components/ProductForm.vue | 110 ++++++++ apps/shop-admin/composables/useMoney.ts | 25 ++ apps/shop-admin/i18n.config.ts | 20 +- apps/shop-admin/locales-extra.ts | 125 +++++++++ apps/shop-admin/middleware/auth.ts | 22 ++ apps/shop-admin/nuxt.config.ts | 3 +- apps/shop-admin/pages/index.vue | 127 ++++++++- apps/shop-admin/pages/invoices.vue | 91 +++++++ apps/shop-admin/pages/login.vue | 47 ++++ apps/shop-admin/pages/orders/[id].vue | 245 ++++++++++++++++++ apps/shop-admin/pages/orders/index.vue | 110 ++++++++ apps/shop-admin/pages/products/[id].vue | 199 ++++++++++++++ apps/shop-admin/pages/products/index.vue | 137 ++++++++++ apps/shop-admin/pages/products/new.vue | 49 ++++ apps/shop-admin/pages/shipments.vue | 88 +++++++ .../proposal.md | 0 .../specs/cart/spec.md | 0 .../specs/order/spec.md | 0 .../2026-09-17-cart-checkout-orders/tasks.md | 17 ++ .../proposal.md | 0 .../specs/catalog/spec.md | 0 .../specs/currency/spec.md | 0 .../2026-09-17-catalog-i18n-currency/tasks.md | 22 ++ .../2026-09-17-foundation-auth}/proposal.md | 0 .../specs/auth/spec.md | 0 .../specs/rbac/spec.md | 0 .../2026-09-17-foundation-auth}/tasks.md | 14 +- .../2026-09-17-frontend-apps/agent-admin.md | 21 ++ .../agent-conventions.md | 27 ++ .../2026-09-17-frontend-apps/agent-mall.md | 24 ++ .../agent-shop-admin.md | 25 ++ .../2026-09-17-frontend-apps}/proposal.md | 0 .../specs/frontend-admin/spec.md | 0 .../specs/frontend-mall/spec.md | 0 .../specs/frontend-shop-admin/spec.md | 0 .../archive/2026-09-17-frontend-apps/tasks.md | 22 ++ .../proposal.md | 0 .../specs/invoice/spec.md | 0 .../specs/shipment/spec.md | 0 .../2026-09-17-fulfillment-invoices/tasks.md | 17 ++ .../changes/cart-checkout-orders/tasks.md | 17 -- .../changes/catalog-i18n-currency/tasks.md | 22 -- openspec/changes/frontend-apps/tasks.md | 22 -- .../changes/fulfillment-invoices/tasks.md | 17 -- openspec/specs/auth/spec.md | 34 +++ openspec/specs/cart/spec.md | 16 ++ openspec/specs/catalog/spec.md | 39 +++ openspec/specs/currency/spec.md | 23 ++ openspec/specs/frontend-admin/spec.md | 19 ++ openspec/specs/frontend-mall/spec.md | 26 ++ openspec/specs/frontend-shop-admin/spec.md | 19 ++ openspec/specs/invoice/spec.md | 23 ++ openspec/specs/order/spec.md | 34 +++ openspec/specs/rbac/spec.md | 23 ++ openspec/specs/shipment/spec.md | 25 ++ packages/shared/src/api.ts | 8 + scripts/seed-demo.mjs | 167 ++++++++++++ 85 files changed, 4327 insertions(+), 106 deletions(-) create mode 100644 apps/admin/locales-extra.ts create mode 100644 apps/admin/middleware/auth.ts create mode 100644 apps/admin/pages/currencies.vue create mode 100644 apps/admin/pages/login.vue create mode 100644 apps/admin/pages/orders.vue create mode 100644 apps/admin/pages/shops.vue create mode 100644 apps/admin/pages/users.vue create mode 100644 apps/mall/components/PriceText.vue create mode 100644 apps/mall/components/StatusBadge.vue create mode 100644 apps/mall/composables/usePrice.ts create mode 100644 apps/mall/locales-extra.ts create mode 100644 apps/mall/middleware/auth.ts create mode 100644 apps/mall/pages/cart.vue create mode 100644 apps/mall/pages/checkout.vue create mode 100644 apps/mall/pages/invoices.vue create mode 100644 apps/mall/pages/login.vue create mode 100644 apps/mall/pages/orders/[id].vue create mode 100644 apps/mall/pages/orders/index.vue create mode 100644 apps/mall/pages/products/[id].vue create mode 100644 apps/mall/pages/register.vue create mode 100644 apps/shop-admin/components/ProductForm.vue create mode 100644 apps/shop-admin/composables/useMoney.ts create mode 100644 apps/shop-admin/locales-extra.ts create mode 100644 apps/shop-admin/middleware/auth.ts create mode 100644 apps/shop-admin/pages/invoices.vue create mode 100644 apps/shop-admin/pages/login.vue create mode 100644 apps/shop-admin/pages/orders/[id].vue create mode 100644 apps/shop-admin/pages/orders/index.vue create mode 100644 apps/shop-admin/pages/products/[id].vue create mode 100644 apps/shop-admin/pages/products/index.vue create mode 100644 apps/shop-admin/pages/products/new.vue create mode 100644 apps/shop-admin/pages/shipments.vue rename openspec/changes/{cart-checkout-orders => archive/2026-09-17-cart-checkout-orders}/proposal.md (100%) rename openspec/changes/{cart-checkout-orders => archive/2026-09-17-cart-checkout-orders}/specs/cart/spec.md (100%) rename openspec/changes/{cart-checkout-orders => archive/2026-09-17-cart-checkout-orders}/specs/order/spec.md (100%) create mode 100644 openspec/changes/archive/2026-09-17-cart-checkout-orders/tasks.md rename openspec/changes/{catalog-i18n-currency => archive/2026-09-17-catalog-i18n-currency}/proposal.md (100%) rename openspec/changes/{catalog-i18n-currency => archive/2026-09-17-catalog-i18n-currency}/specs/catalog/spec.md (100%) rename openspec/changes/{catalog-i18n-currency => archive/2026-09-17-catalog-i18n-currency}/specs/currency/spec.md (100%) create mode 100644 openspec/changes/archive/2026-09-17-catalog-i18n-currency/tasks.md rename openspec/changes/{foundation-auth => archive/2026-09-17-foundation-auth}/proposal.md (100%) rename openspec/changes/{foundation-auth => archive/2026-09-17-foundation-auth}/specs/auth/spec.md (100%) rename openspec/changes/{foundation-auth => archive/2026-09-17-foundation-auth}/specs/rbac/spec.md (100%) rename openspec/changes/{foundation-auth => archive/2026-09-17-foundation-auth}/tasks.md (53%) create mode 100644 openspec/changes/archive/2026-09-17-frontend-apps/agent-admin.md create mode 100644 openspec/changes/archive/2026-09-17-frontend-apps/agent-conventions.md create mode 100644 openspec/changes/archive/2026-09-17-frontend-apps/agent-mall.md create mode 100644 openspec/changes/archive/2026-09-17-frontend-apps/agent-shop-admin.md rename openspec/changes/{frontend-apps => archive/2026-09-17-frontend-apps}/proposal.md (100%) rename openspec/changes/{frontend-apps => archive/2026-09-17-frontend-apps}/specs/frontend-admin/spec.md (100%) rename openspec/changes/{frontend-apps => archive/2026-09-17-frontend-apps}/specs/frontend-mall/spec.md (100%) rename openspec/changes/{frontend-apps => archive/2026-09-17-frontend-apps}/specs/frontend-shop-admin/spec.md (100%) create mode 100644 openspec/changes/archive/2026-09-17-frontend-apps/tasks.md rename openspec/changes/{fulfillment-invoices => archive/2026-09-17-fulfillment-invoices}/proposal.md (100%) rename openspec/changes/{fulfillment-invoices => archive/2026-09-17-fulfillment-invoices}/specs/invoice/spec.md (100%) rename openspec/changes/{fulfillment-invoices => archive/2026-09-17-fulfillment-invoices}/specs/shipment/spec.md (100%) create mode 100644 openspec/changes/archive/2026-09-17-fulfillment-invoices/tasks.md delete mode 100644 openspec/changes/cart-checkout-orders/tasks.md delete mode 100644 openspec/changes/catalog-i18n-currency/tasks.md delete mode 100644 openspec/changes/frontend-apps/tasks.md delete mode 100644 openspec/changes/fulfillment-invoices/tasks.md create mode 100644 openspec/specs/auth/spec.md create mode 100644 openspec/specs/cart/spec.md create mode 100644 openspec/specs/catalog/spec.md create mode 100644 openspec/specs/currency/spec.md create mode 100644 openspec/specs/frontend-admin/spec.md create mode 100644 openspec/specs/frontend-mall/spec.md create mode 100644 openspec/specs/frontend-shop-admin/spec.md create mode 100644 openspec/specs/invoice/spec.md create mode 100644 openspec/specs/order/spec.md create mode 100644 openspec/specs/rbac/spec.md create mode 100644 openspec/specs/shipment/spec.md create mode 100755 scripts/seed-demo.mjs diff --git a/apps/admin/i18n.config.ts b/apps/admin/i18n.config.ts index bcfb01f..af9fa0c 100644 --- a/apps/admin/i18n.config.ts +++ b/apps/admin/i18n.config.ts @@ -1,8 +1,24 @@ -import { en, zh } from "@vmall/shared/locales"; +import { en as enBase, zh as zhBase } from "@vmall/shared/locales"; +import { enExtra, zhExtra } from "./locales-extra"; + +function deepMerge>(base: T, extra: T): T { + const out: Record = { ...base }; + for (const [k, v] of Object.entries(extra)) { + const b = out[k]; + out[k] = + b && v && typeof b === "object" && typeof v === "object" + ? deepMerge(b as Record, v as Record) + : v; + } + return out as T; +} export default defineI18nConfig(() => ({ legacy: false, locale: "en", fallbackLocale: "en", - messages: { en, zh }, + messages: { + en: deepMerge(enBase, enExtra), + zh: deepMerge(zhBase, zhExtra), + }, })); diff --git a/apps/admin/locales-extra.ts b/apps/admin/locales-extra.ts new file mode 100644 index 0000000..271fbf8 --- /dev/null +++ b/apps/admin/locales-extra.ts @@ -0,0 +1,67 @@ +// App-local message extensions, deep-merged over @vmall/shared locales. +// Add keys here (never edit the shared locale files from an app agent). +export const enExtra = { + auth: { + adminHint: "Dev account: admin@vmall.local / admin1234", + wrongRole: "This account does not have platform administrator access.", + }, + admin: { + dashboardTitle: "Platform overview", + refresh: "Refresh", + open: "Open", + totalUsers: "Total users", + totalShops: "Total shops", + totalOrders: "Total orders", + enabledCurrencies: "Enabled currencies", + shopRequired: "Select a shop for shop owners and staff.", + noAssignedShop: "No shop assigned", + created: "Created", + orderShop: "Shop", + orderUser: "User", + orderTotal: "Total", + shopStatus: "Shop status", + currencyCode: "Currency code", + currencyNameEn: "Currency name (English)", + currencyNameZh: "Currency name (中文)", + codeInvalid: "Currency code must be three letters.", + currencyFormTitle: "Add currency", + userManagement: "User management", + shopManagement: "Shop management", + orderManagement: "Order management", + currencyManagement: "Currency management", + pageOf: "Page {page} of {total}", + }, +} as const; + +export const zhExtra = { + auth: { + adminHint: "开发账号:admin@vmall.local / admin1234", + wrongRole: "此账号没有平台管理员权限。", + }, + admin: { + dashboardTitle: "平台概览", + refresh: "刷新", + open: "打开", + totalUsers: "用户总数", + totalShops: "店铺总数", + totalOrders: "订单总数", + enabledCurrencies: "启用币种", + shopRequired: "店主和店员必须选择所属店铺。", + noAssignedShop: "未分配店铺", + created: "创建时间", + orderShop: "店铺", + orderUser: "用户", + orderTotal: "合计", + shopStatus: "店铺状态", + currencyCode: "币种代码", + currencyNameEn: "币种名称(英文)", + currencyNameZh: "币种名称(中文)", + codeInvalid: "币种代码必须是三个字母。", + currencyFormTitle: "添加币种", + userManagement: "用户管理", + shopManagement: "店铺管理", + orderManagement: "订单管理", + currencyManagement: "币种管理", + pageOf: "第 {page} 页,共 {total} 页", + }, +} as const; diff --git a/apps/admin/middleware/auth.ts b/apps/admin/middleware/auth.ts new file mode 100644 index 0000000..c792290 --- /dev/null +++ b/apps/admin/middleware/auth.ts @@ -0,0 +1,11 @@ +import { readStoredUser } from "../stores/session"; + +export default defineNuxtRouteMiddleware(() => { + if (!import.meta.client) return; + + const token = localStorage.getItem("vmall.token"); + const user = readStoredUser(); + if (!token || !user || user.role !== "platform_admin") { + return navigateTo("/login"); + } +}); diff --git a/apps/admin/nuxt.config.ts b/apps/admin/nuxt.config.ts index e4657d1..8c43233 100644 --- a/apps/admin/nuxt.config.ts +++ b/apps/admin/nuxt.config.ts @@ -16,7 +16,7 @@ export default defineNuxtConfig({ { code: "en", name: "English" }, { code: "zh", name: "中文" }, ], - vueI18n: "./i18n.config.ts", + vueI18n: "../i18n.config.ts", }, compatibilityDate: "2025-01-01", }); diff --git a/apps/admin/pages/currencies.vue b/apps/admin/pages/currencies.vue new file mode 100644 index 0000000..1bdfe8b --- /dev/null +++ b/apps/admin/pages/currencies.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/apps/admin/pages/index.vue b/apps/admin/pages/index.vue index d286cfd..44df251 100644 --- a/apps/admin/pages/index.vue +++ b/apps/admin/pages/index.vue @@ -1,6 +1,158 @@ + + + + diff --git a/apps/admin/pages/login.vue b/apps/admin/pages/login.vue new file mode 100644 index 0000000..d0c36b4 --- /dev/null +++ b/apps/admin/pages/login.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/apps/admin/pages/orders.vue b/apps/admin/pages/orders.vue new file mode 100644 index 0000000..f3881ef --- /dev/null +++ b/apps/admin/pages/orders.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/apps/admin/pages/shops.vue b/apps/admin/pages/shops.vue new file mode 100644 index 0000000..7dfac14 --- /dev/null +++ b/apps/admin/pages/shops.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/apps/admin/pages/users.vue b/apps/admin/pages/users.vue new file mode 100644 index 0000000..e0f21a6 --- /dev/null +++ b/apps/admin/pages/users.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/apps/api/src/money.rs b/apps/api/src/money.rs index 8e3c5e9..b9392f3 100644 --- a/apps/api/src/money.rs +++ b/apps/api/src/money.rs @@ -1,5 +1,5 @@ use rust_decimal::prelude::*; -use rust_decimal::Decimal; +use rust_decimal::{Decimal, RoundingStrategy}; use crate::error::ApiError; use crate::models::Currency; @@ -16,7 +16,8 @@ pub fn convert_minor(amount_minor: i64, from: &Currency, to: &Currency) -> Resul let to_scale = Decimal::from(10i64.pow(to.exponent as u32)); // minor(from) -> major(from) -> major(base) -> minor(to) let base_major = amount / from_scale / from.rate_to_base; - let target = (base_major * to.rate_to_base * to_scale).round_dp(0); + let target = (base_major * to.rate_to_base * to_scale) + .round_dp_with_strategy(0, RoundingStrategy::MidpointAwayFromZero); target .to_i64() .ok_or_else(|| ApiError::BadRequest("amount out of range".into())) diff --git a/apps/api/tests/catalog.rs b/apps/api/tests/catalog.rs index 18a9250..9eee5e7 100644 --- a/apps/api/tests/catalog.rs +++ b/apps/api/tests/catalog.rs @@ -146,6 +146,15 @@ async fn currency_conversion_math() { assert_eq!(body["amount_minor"], 1500); assert_eq!(body["currency"], "JPY"); + // midpoint rounds half-up (away from zero), not banker's rounding: + // 1999 minor USD ($19.99) -> JPY at rate 150 => 2998.5 -> 2999 + let res = client() + .get(app.url("/api/currencies/convert?amount_minor=1999&from=USD&to=JPY")) + .send() + .await + .unwrap(); + assert_eq!(res.json::().await.unwrap()["amount_minor"], 2999); + // disabled currency rejected let admin = login_admin(&app).await; let res = client() diff --git a/apps/mall/components/PriceText.vue b/apps/mall/components/PriceText.vue new file mode 100644 index 0000000..66501c6 --- /dev/null +++ b/apps/mall/components/PriceText.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/mall/components/StatusBadge.vue b/apps/mall/components/StatusBadge.vue new file mode 100644 index 0000000..913a251 --- /dev/null +++ b/apps/mall/components/StatusBadge.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/mall/composables/usePrice.ts b/apps/mall/composables/usePrice.ts new file mode 100644 index 0000000..0fa9a12 --- /dev/null +++ b/apps/mall/composables/usePrice.ts @@ -0,0 +1,60 @@ +import { formatMoney } from "@vmall/shared"; +import type { Currency } from "@vmall/shared"; + +export function usePrice() { + const { currency } = usePrefs(); + const { locale } = useI18n(); + const { $api } = useNuxtApp(); + const currencies = useState("mall-currencies", () => []); + const cache = useState>("mall-price-cache", () => new Map()); + const currencyRequest = useState | null>("mall-currency-request", () => null); + + async function ensureCurrencies(): Promise { + if (currencies.value.length > 0) return currencies.value; + if (!currencyRequest.value) { + currencyRequest.value = $api.listCurrencies().then((list) => { + currencies.value = list; + return list; + }).finally(() => { + currencyRequest.value = null; + }); + } + return currencyRequest.value; + } + + function exponentFor(code: string): number { + return currencies.value.find((item) => item.code === code)?.exponent ?? 2; + } + + async function convertAmount(amountMinor: number, from: string, to = currency.value): Promise { + if (from === to) return amountMinor; + const key = `${amountMinor}:${from}:${to}`; + const cached = cache.value.get(key); + if (cached !== undefined) return cached; + const converted = await $api.convert(amountMinor, from, to); + cache.value.set(key, converted.amount_minor); + return converted.amount_minor; + } + + async function formatPrice(amountMinor: number, from: string, to = currency.value): Promise { + await ensureCurrencies(); + const amount = await convertAmount(amountMinor, from, to); + return formatMoney(amount, to, exponentFor(to), locale.value); + } + + async function formatAmount(amountMinor: number, code: string): Promise { + await ensureCurrencies(); + return formatMoney(amountMinor, code, exponentFor(code), locale.value); + } + + return { + currency, + currencies, + cache, + ensureCurrencies, + exponentFor, + convertAmount, + formatPrice, + formatAmount, + }; +} diff --git a/apps/mall/i18n.config.ts b/apps/mall/i18n.config.ts index bcfb01f..af9fa0c 100644 --- a/apps/mall/i18n.config.ts +++ b/apps/mall/i18n.config.ts @@ -1,8 +1,24 @@ -import { en, zh } from "@vmall/shared/locales"; +import { en as enBase, zh as zhBase } from "@vmall/shared/locales"; +import { enExtra, zhExtra } from "./locales-extra"; + +function deepMerge>(base: T, extra: T): T { + const out: Record = { ...base }; + for (const [k, v] of Object.entries(extra)) { + const b = out[k]; + out[k] = + b && v && typeof b === "object" && typeof v === "object" + ? deepMerge(b as Record, v as Record) + : v; + } + return out as T; +} export default defineI18nConfig(() => ({ legacy: false, locale: "en", fallbackLocale: "en", - messages: { en, zh }, + messages: { + en: deepMerge(enBase, enExtra), + zh: deepMerge(zhBase, zhExtra), + }, })); diff --git a/apps/mall/locales-extra.ts b/apps/mall/locales-extra.ts new file mode 100644 index 0000000..1d112c2 --- /dev/null +++ b/apps/mall/locales-extra.ts @@ -0,0 +1,111 @@ +// App-local message extensions, deep-merged over @vmall/shared locales. +// Add keys here (never edit the shared locale files from an app agent). +export const enExtra = { + mall: { + catalogTitle: "Shop products", + searchPlaceholder: "Search products", + categoryPlaceholder: "Choose a category", + allCategories: "All categories", + clear: "Clear", + viewDetails: "View details", + noImage: "No image", + noProducts: "No products found", + productDescription: "Description", + attributes: "Attributes", + sku: "SKU", + chooseSku: "Choose an option", + quantity: "Quantity", + available: "available", + unavailable: "Unavailable", + addedToCart: "Added to cart", + cartItems: "Cart items", + lineTotal: "Line total", + orderPreview: "Order preview", + checkoutTitle: "Checkout", + placeOrder: "Place order", + orderSuccess: "Your order has been placed", + orderNumbers: "Order numbers", + viewOrders: "View orders", + backToShop: "Continue shopping", + noOrders: "No orders found", + created: "Created", + items: "Items", + address: "Address", + shipments: "Shipments", + invoices: "Invoices", + noInvoice: "No invoice requested", + invoiceRequested: "Invoice requested", + notAvailable: "—", + companyTaxRequired: "Tax number is required for company invoices", + invalidQuantity: "Enter a valid quantity", + loadFailed: "Unable to load data", + signInRequired: "Please sign in to continue", + filter: "Filter", + details: "Details", + update: "Update", + confirm: "Confirm", + totalItems: "Total items", + shippingOptional: "Optional", + orderDetails: "Order details", + payment: "Payment", + requestInvoiceForOrder: "Request an invoice for this order", + }, + auth: { + wrongRole: "This account is not a customer account", + }, +} as Record; + +export const zhExtra = { + mall: { + catalogTitle: "选购商品", + searchPlaceholder: "搜索商品", + categoryPlaceholder: "选择分类", + allCategories: "全部分类", + clear: "清除", + viewDetails: "查看详情", + noImage: "暂无图片", + noProducts: "未找到商品", + productDescription: "商品描述", + attributes: "属性", + sku: "SKU", + chooseSku: "选择规格", + quantity: "数量", + available: "件可用", + unavailable: "暂不可用", + addedToCart: "已加入购物车", + cartItems: "购物车商品", + lineTotal: "行合计", + orderPreview: "订单预览", + checkoutTitle: "结算", + placeOrder: "提交订单", + orderSuccess: "订单提交成功", + orderNumbers: "订单号", + viewOrders: "查看订单", + backToShop: "继续购物", + noOrders: "暂无订单", + created: "创建时间", + items: "商品数", + address: "地址", + shipments: "发货信息", + invoices: "发票", + noInvoice: "尚未申请发票", + invoiceRequested: "发票已申请", + notAvailable: "—", + companyTaxRequired: "企业发票必须填写税号", + invalidQuantity: "请输入有效数量", + loadFailed: "无法加载数据", + signInRequired: "请先登录", + filter: "筛选", + details: "详情", + update: "更新", + confirm: "确认", + totalItems: "商品总数", + shippingOptional: "可选", + orderDetails: "订单详情", + payment: "支付", + requestInvoiceForOrder: "申请本订单发票", + }, + auth: { + wrongRole: "此账号不是买家账号", + }, +} as Record; diff --git a/apps/mall/middleware/auth.ts b/apps/mall/middleware/auth.ts new file mode 100644 index 0000000..3413cd0 --- /dev/null +++ b/apps/mall/middleware/auth.ts @@ -0,0 +1,15 @@ +export default defineNuxtRouteMiddleware(() => { + if (import.meta.server) return; + const token = localStorage.getItem("vmall.token"); + let role: string | null = null; + try { + const raw = localStorage.getItem("vmall.user"); + const user: unknown = raw ? JSON.parse(raw) : null; + if (user && typeof user === "object" && "role" in user && typeof user.role === "string") { + role = user.role; + } + } catch { + role = null; + } + if (!token || role !== "customer") return navigateTo("/login"); +}); diff --git a/apps/mall/nuxt.config.ts b/apps/mall/nuxt.config.ts index 98190ff..9c34e28 100644 --- a/apps/mall/nuxt.config.ts +++ b/apps/mall/nuxt.config.ts @@ -16,7 +16,8 @@ export default defineNuxtConfig({ { code: "en", name: "English" }, { code: "zh", name: "中文" }, ], - vueI18n: "./i18n.config.ts", + vueI18n: "../i18n.config.ts", + bundle: { optimizeTranslationDirective: false }, }, compatibilityDate: "2025-01-01", }); diff --git a/apps/mall/pages/cart.vue b/apps/mall/pages/cart.vue new file mode 100644 index 0000000..161c567 --- /dev/null +++ b/apps/mall/pages/cart.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/apps/mall/pages/checkout.vue b/apps/mall/pages/checkout.vue new file mode 100644 index 0000000..81f711b --- /dev/null +++ b/apps/mall/pages/checkout.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/apps/mall/pages/index.vue b/apps/mall/pages/index.vue index d286cfd..0f9f518 100644 --- a/apps/mall/pages/index.vue +++ b/apps/mall/pages/index.vue @@ -1,6 +1,144 @@ + + + + diff --git a/apps/mall/pages/invoices.vue b/apps/mall/pages/invoices.vue new file mode 100644 index 0000000..1f2ec69 --- /dev/null +++ b/apps/mall/pages/invoices.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/apps/mall/pages/login.vue b/apps/mall/pages/login.vue new file mode 100644 index 0000000..57c3831 --- /dev/null +++ b/apps/mall/pages/login.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/mall/pages/orders/[id].vue b/apps/mall/pages/orders/[id].vue new file mode 100644 index 0000000..8292316 --- /dev/null +++ b/apps/mall/pages/orders/[id].vue @@ -0,0 +1,197 @@ + + + + + diff --git a/apps/mall/pages/orders/index.vue b/apps/mall/pages/orders/index.vue new file mode 100644 index 0000000..7175d38 --- /dev/null +++ b/apps/mall/pages/orders/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/apps/mall/pages/products/[id].vue b/apps/mall/pages/products/[id].vue new file mode 100644 index 0000000..133c4b1 --- /dev/null +++ b/apps/mall/pages/products/[id].vue @@ -0,0 +1,143 @@ + + + + + diff --git a/apps/mall/pages/register.vue b/apps/mall/pages/register.vue new file mode 100644 index 0000000..5819e8b --- /dev/null +++ b/apps/mall/pages/register.vue @@ -0,0 +1,41 @@ + + + diff --git a/apps/shop-admin/components/ProductForm.vue b/apps/shop-admin/components/ProductForm.vue new file mode 100644 index 0000000..762a185 --- /dev/null +++ b/apps/shop-admin/components/ProductForm.vue @@ -0,0 +1,110 @@ + + +