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

204 lines
6.9 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 { Order, OrderStatus } from "@vmall/shared";
import { t as pick } from "@vmall/shared";
definePageMeta({ middleware: "auth" });
const { locale, t } = useI18n();
const { $api } = useNuxtApp();
const route = useRoute();
// Shared with the store directory; orders carry only a shop id.
const { data: shops } = await useAsyncData("shops", () => $api.listShops(), { default: () => [] });
function shopName(shopId: string): string {
const shop = shops.value.find((entry) => entry.id === shopId);
return shop ? pick(shop.name, locale.value) : t("user.shop");
}
const activeFilter = ref("all");
const orders = ref<Order[]>([]);
const page = ref(1);
const total = ref(0);
const perPage = ref(10);
const loading = ref(true);
const workingId = ref("");
const filterStatuses: Record<string, OrderStatus[]> = {
all: [],
pending_payment: ["pending_payment"],
pending_shipment: ["paid", "fulfilling"],
pending_receipt: ["shipped"],
completed: ["completed"],
cancelled: ["cancelled"],
};
const tabs = computed(() => [
{ key: "all", label: t("user.filterAll") },
{ key: "pending_payment", label: t("user.filterPendingPayment") },
{ key: "pending_shipment", label: t("user.filterPendingShipment") },
{ key: "pending_receipt", label: t("user.filterPendingReceipt") },
{ key: "completed", label: t("user.filterCompleted") },
{ key: "cancelled", label: t("user.filterCancelled") },
]);
const filteredOrders = computed(() => {
const statuses = filterStatuses[activeFilter.value] ?? [];
return statuses.length === 0
? orders.value
: orders.value.filter((order) => statuses.includes(order.status));
});
function setInitialFilter(): void {
const queryStatus = String(route.query.status ?? "");
const mapped =
queryStatus === "paid" || queryStatus === "fulfilling"
? "pending_shipment"
: queryStatus === "shipped"
? "pending_receipt"
: queryStatus === "pending_payment" ||
queryStatus === "completed" ||
queryStatus === "cancelled"
? queryStatus
: "all";
activeFilter.value = mapped;
}
async function loadOrders(nextPage = page.value): Promise<void> {
loading.value = true;
try {
const result = await $api.listMyOrders(nextPage);
orders.value = result.items;
page.value = result.page;
total.value = result.total;
perPage.value = result.per_page;
} finally {
loading.value = false;
}
}
async function pay(order: Order): Promise<void> {
workingId.value = order.id;
try {
await $api.payOrder(order.id);
await loadOrders();
} finally {
workingId.value = "";
}
}
async function cancel(order: Order): Promise<void> {
workingId.value = order.id;
try {
await $api.cancelOrder(order.id);
await loadOrders();
} finally {
workingId.value = "";
}
}
function changeFilter(key: string): void {
activeFilter.value = key;
}
onMounted(() => {
setInitialFilter();
void loadOrders();
});
</script>
<template>
<VCard class="min-h-[560px]">
<h1 class="text-text mb-4 text-xl font-bold">{{ t("user.ordersTitle") }}</h1>
<UiTabs :model-value="activeFilter" :tabs="tabs" @update:model-value="changeFilter">
<template #default>
<div v-if="loading" class="text-muted py-[30px]">{{ t("common.loading") }}</div>
<UiEmptyState v-else-if="filteredOrders.length === 0" :text="t('user.noOrders')" />
<div v-else class="space-y-3.5">
<VCard
v-for="order in filteredOrders"
:key="order.id"
:padded="false"
class="overflow-hidden"
>
<header
class="border-border bg-bg text-muted flex items-center justify-between gap-2.5 border-b px-3.5 py-3 text-xs max-sm:items-start"
>
<div
class="flex min-w-0 items-center gap-3.5 max-sm:flex-col max-sm:items-start max-sm:gap-1"
>
<strong class="text-text text-[13px] font-semibold">{{
shopName(order.shop_id)
}}</strong
><span>{{ t("user.orderNo") }} {{ order.order_no }}</span
><span>{{ t("user.createdAt") }} {{ order.created_at.slice(0, 10) }}</span>
</div>
<StatusBadge :status="order.status" kind="order" />
</header>
<div class="px-3.5 py-1">
<div
v-for="item in order.items"
:key="item.id"
class="border-border flex items-center gap-3 border-b py-2.5 last:border-b-0"
>
<img
class="border-border h-[54px] w-[54px] border object-contain"
:src="item.image || '/mock/product-1.svg'"
:alt="pick(item.product_name, locale)"
/>
<div class="flex min-w-0 flex-1 flex-col gap-1 text-[13px]">
<span class="truncate">{{ pick(item.product_name, locale) }}</span
><small class="text-muted text-[11px]"
>{{ item.sku_code }} × {{ item.qty }}</small
>
</div>
<PriceText
class="text-muted text-[13px]"
:amount-minor="item.unit_price_minor"
:currency="order.currency"
/>
</div>
</div>
<footer
class="border-border text-muted flex items-center justify-between gap-3 border-t px-3.5 py-3 text-xs max-sm:flex-col max-sm:items-start"
>
<span
>{{ t("user.orderTotal") }}
<strong class="text-primary"
><PriceText
:amount-minor="order.total_minor"
:currency="order.currency" /></strong
></span>
<div class="flex gap-2">
<VBtn
v-if="order.status === 'pending_payment'"
variant="primary"
size="sm"
type="button"
:disabled="workingId === order.id"
@click="pay(order)"
>{{ t("user.payNow") }}</VBtn
>
<VBtn
v-if="order.status === 'pending_payment'"
size="sm"
type="button"
:disabled="workingId === order.id"
@click="cancel(order)"
>{{ t("user.cancelOrder") }}</VBtn
>
<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>
</footer>
</VCard>
</div>
<UiPagination :page="page" :total="total" :per-page="perPage" @change="loadOrders" />
</template>
</UiTabs>
</VCard>
</template>