Files
vmall/apps/mall/pages/user/orders/index.vue
T
Chengdong Zhang 0d0e10b97b feat(ui): adopt Tailwind v4 design system and archive change
- 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
2026-09-22 18:22:50 +08:00

140 lines
5.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="mb-4 text-xl font-bold text-text">{{ t("user.ordersTitle") }}</h1>
<UiTabs :model-value="activeFilter" :tabs="tabs" @update:model-value="changeFilter">
<template #default>
<div v-if="loading" class="py-[30px] text-muted">{{ 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="flex items-center justify-between gap-2.5 border-b border-border bg-bg px-3.5 py-3 text-xs text-muted 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-[13px] font-semibold text-text">{{ 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="flex items-center gap-3 border-b border-border py-2.5 last:border-b-0">
<img class="h-[54px] w-[54px] border border-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-[11px] text-muted">{{ item.sku_code }} × {{ item.qty }}</small></div>
<PriceText class="text-[13px] text-muted" :amount-minor="item.unit_price_minor" :currency="order.currency" />
</div>
</div>
<footer class="flex items-center justify-between gap-3 border-t border-border px-3.5 py-3 text-xs text-muted 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="inline-flex items-center rounded-md border border-border bg-surface px-2.5 py-1 text-[13px] font-medium text-text 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>