feat: three nuxt frontends, demo seed, rounding + money-exponent + rate-cast fixes, archived specs
This commit is contained in:
@@ -1,6 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { t as localized } from "@vmall/shared";
|
||||
import type { OrderStatus, Shop } from "@vmall/shared";
|
||||
|
||||
definePageMeta({ middleware: "auth" });
|
||||
|
||||
const { $api } = useNuxtApp();
|
||||
const { locale, t: translate } = useI18n();
|
||||
const loading = ref(true);
|
||||
const error = ref("");
|
||||
const shop = ref<Shop | null>(null);
|
||||
const totalProducts = ref(0);
|
||||
const publishedProducts = ref(0);
|
||||
const orderCounts = ref<Record<OrderStatus, number>>({
|
||||
pending_payment: 0,
|
||||
paid: 0,
|
||||
fulfilling: 0,
|
||||
shipped: 0,
|
||||
completed: 0,
|
||||
cancelled: 0,
|
||||
});
|
||||
const pendingInvoices = ref(0);
|
||||
const orderStatuses: OrderStatus[] = ["pending_payment", "paid", "fulfilling", "shipped"];
|
||||
|
||||
async function loadDashboard(): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const [products, published, shopData, invoices, ...orders] = await Promise.all([
|
||||
$api.shop.listMyProducts({ per_page: 100 }),
|
||||
$api.shop.listMyProducts({ per_page: 100, status: "published" }),
|
||||
$api.shop.getMyShop(),
|
||||
$api.shop.listInvoices(),
|
||||
...orderStatuses.map((status) => $api.shop.listOrders({ status, per_page: 100 })),
|
||||
]);
|
||||
totalProducts.value = products.total;
|
||||
publishedProducts.value = published.total;
|
||||
shop.value = shopData;
|
||||
pendingInvoices.value = invoices.filter((invoice) => invoice.status === "requested").length;
|
||||
orderStatuses.forEach((status, index) => {
|
||||
orderCounts.value[status] = orders[index]?.total ?? 0;
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : translate("common.error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadDashboard);
|
||||
|
||||
const stats = computed(() => [
|
||||
{ key: "totalProducts", label: "shop.totalProducts", value: totalProducts.value },
|
||||
{ key: "publishedProducts", label: "shop.publishedProducts", value: publishedProducts.value },
|
||||
{ key: "pendingPayment", label: "shop.pendingPaymentOrders", value: orderCounts.value.pending_payment },
|
||||
{ key: "paid", label: "shop.paidOrders", value: orderCounts.value.paid },
|
||||
{ key: "fulfilling", label: "shop.fulfillingOrders", value: orderCounts.value.fulfilling },
|
||||
{ key: "shipped", label: "shop.shippedOrders", value: orderCounts.value.shipped },
|
||||
{ key: "invoices", label: "shop.pendingInvoices", value: pendingInvoices.value },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="page-title">{{ $t("common.appName") }}</h1>
|
||||
<p class="muted">{{ $t("common.loading") }}</p>
|
||||
<div class="page">
|
||||
<div class="page-head">
|
||||
<h1 class="page-title">{{ $t("shop.dashboardTitle") }}</h1>
|
||||
<button class="btn sm" :disabled="loading" @click="loadDashboard">{{ $t("common.search") }}</button>
|
||||
</div>
|
||||
<div v-if="error" class="error-text" role="alert">{{ error }}</div>
|
||||
<p v-if="loading" class="muted">{{ $t("common.loading") }}</p>
|
||||
<template v-else>
|
||||
<div class="stats-grid">
|
||||
<div v-for="stat in stats" :key="stat.key" class="card stat-card">
|
||||
<span class="muted">{{ $t(stat.label) }}</span>
|
||||
<strong>{{ stat.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<section v-if="shop" class="card shop-profile mt">
|
||||
<h2>{{ $t("shop.profile") }}</h2>
|
||||
<div class="profile-grid">
|
||||
<div>
|
||||
<span class="muted">{{ $t("product.shop") }}</span>
|
||||
<strong>{{ localized(shop.name, locale) }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="muted">{{ $t("shop.slug") }}</span>
|
||||
<strong>{{ shop.slug }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="muted">{{ $t("shop.shopStatus") }}</span>
|
||||
<span class="badge" :class="shop.status === 'active' ? 'green' : 'red'">
|
||||
{{ $t(`admin.${shop.status}`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.stat-card strong {
|
||||
font-size: 28px;
|
||||
}
|
||||
.shop-profile h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.profile-grid > div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user