204 lines
6.9 KiB
Vue
204 lines
6.9 KiB
Vue
<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>
|