- Add tailwindcss v4 + @tailwindcss/vite to mall, shop-admin, admin - Add @vmall/shared/theme.css tokens with html[data-accent] presets - Add @vmall/ui kit (VBtn/VBadge/VField/VInput/VCard/VPanel/VTable/VPage, VAccentSwatch, useAccent) as a Nuxt module - Convert all three apps to kit + utilities; delete ui.css/mall.css and every <style scoped>; consoles get accent presets, mall locked to red - Fix VCard boolean prop default (padding) and PDP/store stale useAsyncData keys on param navigation - Archive adopt-tailwind-design-system; new frontend-ui capability spec
99 lines
3.9 KiB
Vue
99 lines
3.9 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>
|
|
<VPage :title="$t('shop.dashboardTitle')">
|
|
<template #actions>
|
|
<VBtn size="sm" :disabled="loading" @click="loadDashboard">{{ $t("common.search") }}</VBtn>
|
|
</template>
|
|
<div v-if="error" class="my-2 text-sm text-danger" role="alert">{{ error }}</div>
|
|
<p v-if="loading" class="text-muted">{{ $t("common.loading") }}</p>
|
|
<template v-else>
|
|
<div class="grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(170px,1fr))]">
|
|
<VCard v-for="stat in stats" :key="stat.key" class="grid gap-2">
|
|
<span class="text-sm text-muted">{{ $t(stat.label) }}</span>
|
|
<strong class="text-3xl font-bold text-text">{{ stat.value }}</strong>
|
|
</VCard>
|
|
</div>
|
|
<VCard v-if="shop" class="mt-4">
|
|
<h2 class="mb-4 text-lg font-semibold text-text">{{ $t("shop.profile") }}</h2>
|
|
<div class="grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
|
|
<div class="grid gap-1.5">
|
|
<span class="text-sm text-muted">{{ $t("product.shop") }}</span>
|
|
<strong>{{ localized(shop.name, locale) }}</strong>
|
|
</div>
|
|
<div class="grid gap-1.5">
|
|
<span class="text-sm text-muted">{{ $t("shop.slug") }}</span>
|
|
<strong>{{ shop.slug }}</strong>
|
|
</div>
|
|
<div class="grid gap-1.5">
|
|
<span class="text-sm text-muted">{{ $t("shop.shopStatus") }}</span>
|
|
<VBadge :tone="shop.status === 'active' ? 'green' : 'red'">
|
|
{{ $t(`admin.${shop.status}`) }}
|
|
</VBadge>
|
|
</div>
|
|
</div>
|
|
</VCard>
|
|
</template>
|
|
</VPage>
|
|
</template>
|