Files
vmall/apps/mall/pages/user/index.vue
T

218 lines
7.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { AccountSummary, Order, ProductFavorite } from "@vmall/shared";
import { t as pick } from "@vmall/shared";
definePageMeta({ middleware: "auth" });
const { locale, t } = useI18n();
const { $api } = useNuxtApp();
const orders = ref<Order[]>([]);
const stats = ref<AccountSummary | null>(null);
const favoriteProducts = ref<ProductFavorite[]>([]);
const favoriteProductTotal = ref(0);
const loading = ref(true);
async function loadOrders(): Promise<void> {
loading.value = true;
try {
const result = await $api.listMyOrders();
orders.value = result.items;
} finally {
loading.value = false;
}
}
// Live balances; shows a placeholder rather than a fixture if the call fails.
async function loadStats(): Promise<void> {
try {
stats.value = await $api.getAccountSummary();
} catch {
stats.value = null;
}
}
async function loadFavorites(): Promise<void> {
try {
const page = await $api.listFavorites({ kind: "product", page: 1, per_page: 8 });
favoriteProducts.value = page.items.filter(
(row): row is ProductFavorite => row.kind === "product",
);
favoriteProductTotal.value = page.total;
} catch {
favoriteProducts.value = [];
favoriteProductTotal.value = 0;
}
}
onMounted(() => {
void loadOrders();
void loadStats();
void loadFavorites();
});
const counts = computed(() => ({
pendingPayment: orders.value.filter((o) => o.status === "pending_payment").length,
pendingShipment: orders.value.filter((o) => o.status === "paid").length,
pendingReceipt: orders.value.filter((o) => o.status === "shipped").length,
completed: orders.value.filter((o) => o.status === "completed").length,
afterSale: 0,
}));
const statusLinks = computed(() => [
{
key: "pendingPayment",
label: t("user.pendingPayment"),
count: counts.value.pendingPayment,
to: "/user/orders?status=pending_payment",
},
{
key: "pendingShipment",
label: t("user.pendingShipment"),
count: counts.value.pendingShipment,
to: "/user/orders?status=paid",
},
{
key: "pendingReceipt",
label: t("user.pendingReceipt"),
count: counts.value.pendingReceipt,
to: "/user/orders?status=shipped",
},
{
key: "completed",
label: t("user.completed"),
count: counts.value.completed,
to: "/user/orders?status=completed",
},
{
key: "afterSale",
label: t("user.afterSale"),
count: counts.value.afterSale,
to: "/user/orders?status=after-sale",
},
]);
</script>
<template>
<div class="space-y-4">
<VCard class="pb-0">
<h1 class="text-text mb-4 text-xl font-bold">{{ t("user.dashboard") }}</h1>
<div class="mb-5 grid gap-3 sm:grid-cols-3">
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsBalance") }}</span
><strong class="text-primary mt-2.5 block text-xl"
><PriceText
v-if="stats"
:amount-minor="stats.balance_minor"
:currency="stats.currency"
/><template v-else>{{ t("common.loading") }}</template></strong
>
</div>
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsPoints") }}</span
><strong class="text-primary mt-2.5 block text-xl">{{
stats ? stats.points : t("common.loading")
}}</strong>
</div>
<div class="border-border bg-primary-soft border p-[18px]">
<span class="text-muted block text-xs">{{ t("user.statsFrozen") }}</span
><strong class="text-primary mt-2.5 block text-xl"
><PriceText
v-if="stats"
:amount-minor="stats.frozen_minor"
:currency="stats.currency"
/><template v-else>{{ t("common.loading") }}</template></strong
>
</div>
</div>
<div class="border-border grid grid-cols-3 border-t sm:grid-cols-5">
<NuxtLink
v-for="item in statusLinks"
:key="item.key"
:to="item.to"
class="text-muted hover:text-primary sm:border-border flex flex-col items-center gap-2 px-2 py-4 text-xs no-underline sm:border-l first:sm:border-l-0"
><span>{{ item.label }}</span
><b class="text-primary text-lg font-semibold">{{ item.count }}</b></NuxtLink
>
</div>
</VCard>
<VCard>
<h2 class="text-text mb-4 flex items-center justify-between text-lg font-semibold">
{{ t("user.recentOrders") }}
<NuxtLink class="text-primary text-sm font-normal no-underline" to="/user/orders"
>{{ t("user.viewAll") }} ›</NuxtLink
>
</h2>
<div v-if="loading" class="text-muted py-5">{{ t("common.loading") }}</div>
<UiEmptyState v-else-if="orders.length === 0" :text="t('user.noOrders')" />
<div v-else class="space-y-3">
<article v-for="order in orders.slice(0, 3)" :key="order.id" class="border-border border">
<header
class="bg-bg text-muted flex items-center justify-between gap-3 px-3 py-2.5 text-xs"
>
<span>{{ t("user.orderNo") }} {{ order.order_no }}</span
><StatusBadge :status="order.status" kind="order" />
</header>
<div class="flex items-center gap-4 p-3 max-sm:flex-wrap">
<div class="flex min-w-[140px] gap-1">
<img
v-for="item in order.items"
:key="item.id"
class="border-border h-11 w-11 border object-contain"
:src="item.image || '/mock/product-1.svg'"
:alt="pick(item.product_name, locale)"
/>
</div>
<div class="text-muted flex-1 text-xs">
{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}
</div>
<div class="text-right text-xs">
<span class="text-muted mb-1 block">{{ t("user.orderTotal") }}</span
><PriceText
class="text-primary font-semibold"
:amount-minor="order.total_minor"
:currency="order.currency"
/>
</div>
<NuxtLink
class="border-border bg-surface text-text inline-flex items-center rounded-md border px-2.5 py-1 text-[13px] font-medium no-underline"
:to="`/user/orders/${order.id}`"
>{{ t("user.viewDetails") }}</NuxtLink
>
</div>
</article>
</div>
</VCard>
<VCard>
<h2 class="text-text mb-4 text-lg font-semibold">
{{ t("user.favoriteProducts") }} ({{ favoriteProductTotal }})
</h2>
<UiEmptyState v-if="favoriteProducts.length === 0" :text="t('user.noFavorites')" />
<div v-else class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<NuxtLink
v-for="item in favoriteProducts"
:key="item.id"
:to="`/goods/${item.product.slug}`"
class="border-border text-text hover:border-primary block border p-2.5 no-underline"
>
<img
class="mb-2 block h-[120px] w-full object-contain"
:src="item.product.image || '/mock/product-1.svg'"
:alt="pick(item.product.name, locale)"
/>
<span class="block h-[34px] overflow-hidden text-xs leading-[17px]">{{
pick(item.product.name, locale)
}}</span>
<PriceText
v-if="item.product.price_minor !== null && item.product.currency"
class="text-primary mt-2 block text-sm font-semibold"
:amount-minor="item.product.price_minor"
:currency="item.product.currency"
/>
</NuxtLink>
</div>
</VCard>
</div>
</template>