- 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
91 lines
3.3 KiB
Vue
91 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
|
|
const { t } = useI18n();
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const { $api } = useNuxtApp();
|
|
|
|
const loading = ref(true);
|
|
const errorMessage = ref("");
|
|
const stats = reactive({
|
|
users: 0,
|
|
shops: 0,
|
|
orders: 0,
|
|
currencies: 0,
|
|
});
|
|
|
|
async function loadDashboard(): Promise<void> {
|
|
loading.value = true;
|
|
errorMessage.value = "";
|
|
try {
|
|
const [users, shops, orders, currencies] = await Promise.all([
|
|
$api.admin.listUsers(1),
|
|
$api.admin.listShops(),
|
|
$api.admin.listOrders(1),
|
|
$api.admin.listCurrencies(),
|
|
]);
|
|
stats.users = users.total;
|
|
stats.shops = shops.length;
|
|
stats.orders = orders.total;
|
|
stats.currencies = currencies.filter((currency) => currency.enabled).length;
|
|
} catch (error: unknown) {
|
|
errorMessage.value = error instanceof Error ? error.message : t("common.error");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadDashboard();
|
|
});
|
|
|
|
const cards = computed(() => [
|
|
{ label: "admin.totalUsers", value: stats.users, link: "/users", tone: "blue" },
|
|
{ label: "admin.totalShops", value: stats.shops, link: "/shops", tone: "green" },
|
|
{ label: "admin.totalOrders", value: stats.orders, link: "/orders", tone: "orange" },
|
|
{ label: "admin.enabledCurrencies", value: stats.currencies, link: "/currencies", tone: "purple" },
|
|
]);
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<VPage :title="$t('admin.dashboardTitle')">
|
|
<template #actions>
|
|
<VBtn size="sm" :disabled="loading" @click="loadDashboard">{{ $t("admin.refresh") }}</VBtn>
|
|
</template>
|
|
<p class="mb-4 text-xs font-bold uppercase tracking-[0.08em] text-primary">{{ $t("common.appName") }} · Admin</p>
|
|
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
|
|
<p v-else-if="errorMessage" class="my-2 text-sm text-danger" role="alert">{{ errorMessage }}</p>
|
|
<template v-else>
|
|
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<NuxtLink
|
|
v-for="card in cards"
|
|
:key="card.link"
|
|
:to="card.link"
|
|
class="flex flex-col gap-2 rounded-lg border border-border bg-surface p-5 shadow-sm transition-transform hover:-translate-y-0.5 hover:border-primary"
|
|
>
|
|
<span class="text-[13px] text-muted">{{ $t(card.label) }}</span>
|
|
<strong
|
|
class="text-[32px] leading-none"
|
|
:class="card.tone === 'green' ? 'text-success' : card.tone === 'orange' ? 'text-warning' : 'text-primary'"
|
|
>{{ card.value }}</strong>
|
|
<span class="text-xs text-primary">{{ $t("admin.open") }} →</span>
|
|
</NuxtLink>
|
|
</div>
|
|
<div class="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<NuxtLink v-for="shortcut in [
|
|
{ to: '/users', title: 'admin.userManagement', label: 'nav.users' },
|
|
{ to: '/shops', title: 'admin.shopManagement', label: 'nav.shops' },
|
|
{ to: '/orders', title: 'admin.orderManagement', label: 'nav.orders' },
|
|
{ to: '/currencies', title: 'admin.currencyManagement', label: 'nav.currencies' },
|
|
]" :key="shortcut.to" :to="shortcut.to" class="block no-underline hover:no-underline">
|
|
<VCard>
|
|
<strong class="block">{{ $t(shortcut.title) }}</strong>
|
|
<span class="text-sm text-muted">{{ $t(shortcut.label) }}</span>
|
|
</VCard>
|
|
</NuxtLink>
|
|
</div>
|
|
</template>
|
|
</VPage>
|
|
</template>
|