159 lines
4.5 KiB
Vue
159 lines
4.5 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>
|
|
<div class="page">
|
|
<div class="page-head">
|
|
<div>
|
|
<p class="eyebrow">{{ $t("common.appName") }} · Admin</p>
|
|
<h1 class="page-title">{{ $t("admin.dashboardTitle") }}</h1>
|
|
</div>
|
|
<button class="btn sm" type="button" :disabled="loading" @click="loadDashboard">
|
|
{{ $t("admin.refresh") }}
|
|
</button>
|
|
</div>
|
|
<p v-if="loading" class="muted">{{ $t("common.loading") }}</p>
|
|
<p v-else-if="errorMessage" class="error-text" role="alert">{{ errorMessage }}</p>
|
|
<template v-else>
|
|
<div class="stat-grid">
|
|
<NuxtLink v-for="card in cards" :key="card.link" :to="card.link" class="stat-card">
|
|
<span class="stat-label">{{ $t(card.label) }}</span>
|
|
<strong class="stat-value" :class="`tone-${card.tone}`">{{ card.value }}</strong>
|
|
<span class="stat-link">{{ $t("admin.open") }} →</span>
|
|
</NuxtLink>
|
|
</div>
|
|
<div class="grid shortcuts">
|
|
<NuxtLink class="card shortcut" to="/users">
|
|
<strong>{{ $t("admin.userManagement") }}</strong>
|
|
<span class="muted">{{ $t("nav.users") }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink class="card shortcut" to="/shops">
|
|
<strong>{{ $t("admin.shopManagement") }}</strong>
|
|
<span class="muted">{{ $t("nav.shops") }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink class="card shortcut" to="/orders">
|
|
<strong>{{ $t("admin.orderManagement") }}</strong>
|
|
<span class="muted">{{ $t("nav.orders") }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink class="card shortcut" to="/currencies">
|
|
<strong>{{ $t("admin.currencyManagement") }}</strong>
|
|
<span class="muted">{{ $t("nav.currencies") }}</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.eyebrow {
|
|
color: var(--primary);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
margin: 0 0 6px;
|
|
text-transform: uppercase;
|
|
}
|
|
.stat-grid {
|
|
display: grid;
|
|
gap: 16px;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
.stat-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 20px;
|
|
text-decoration: none;
|
|
transition: transform 150ms ease, border-color 150ms ease;
|
|
}
|
|
.stat-card:hover {
|
|
border-color: var(--primary);
|
|
text-decoration: none;
|
|
transform: translateY(-2px);
|
|
}
|
|
.stat-label {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
}
|
|
.stat-value {
|
|
font-size: 32px;
|
|
line-height: 1;
|
|
}
|
|
.tone-blue { color: var(--primary); }
|
|
.tone-green { color: var(--success); }
|
|
.tone-orange { color: var(--warning); }
|
|
.tone-purple { color: #7257b8; }
|
|
.stat-link {
|
|
color: var(--primary);
|
|
font-size: 12px;
|
|
}
|
|
.shortcuts {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
margin-top: 24px;
|
|
}
|
|
.shortcut {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
text-decoration: none;
|
|
}
|
|
.shortcut:hover { text-decoration: none; }
|
|
@media (max-width: 900px) {
|
|
.stat-grid, .shortcuts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
}
|
|
@media (max-width: 560px) {
|
|
.stat-grid, .shortcuts { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|