128 lines
4.1 KiB
Vue
128 lines
4.1 KiB
Vue
<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 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>
|