143 lines
4.4 KiB
Vue
143 lines
4.4 KiB
Vue
<script setup lang="ts">
|
|
import { formatMoney, t as localizedText } from "@vmall/shared";
|
|
import type { Currency, Order, Shop } from "@vmall/shared";
|
|
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const { $api } = useNuxtApp();
|
|
const { locale, t } = useI18n();
|
|
|
|
const orders = ref<Order[]>([]);
|
|
const shops = ref<Shop[]>([]);
|
|
const currencies = ref<Currency[]>([]);
|
|
const page = ref(1);
|
|
const total = ref(0);
|
|
const perPage = ref(20);
|
|
const loading = ref(true);
|
|
const errorMessage = ref("");
|
|
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / perPage.value)));
|
|
|
|
function shopName(shopId: string): string {
|
|
const shop = shops.value.find((item) => item.id === shopId);
|
|
return shop ? localizedText(shop.name, locale.value) : shopId;
|
|
}
|
|
|
|
function currencyExponent(code: string): number {
|
|
const currency = currencies.value.find((item) => item.code === code);
|
|
return currency?.exponent ?? 2;
|
|
}
|
|
|
|
function formatTotal(order: Order): string {
|
|
return formatMoney(
|
|
order.total_minor,
|
|
order.currency,
|
|
currencyExponent(order.currency),
|
|
locale.value,
|
|
);
|
|
}
|
|
|
|
function formatDate(value: string): string {
|
|
return new Date(value).toLocaleString(locale.value === "zh" ? "zh-CN" : "en-US");
|
|
}
|
|
|
|
function statusClass(status: Order["status"]): "green" | "red" | "orange" | "blue" {
|
|
if (status === "completed") return "green";
|
|
if (status === "cancelled") return "red";
|
|
if (status === "pending_payment") return "orange";
|
|
return "blue";
|
|
}
|
|
|
|
async function loadOrders(): Promise<void> {
|
|
loading.value = true;
|
|
errorMessage.value = "";
|
|
try {
|
|
const [paged, shopList, currencyList] = await Promise.all([
|
|
$api.admin.listOrders(page.value),
|
|
$api.admin.listShops(),
|
|
$api.admin.listCurrencies(),
|
|
]);
|
|
orders.value = paged.items;
|
|
total.value = paged.total;
|
|
perPage.value = paged.per_page;
|
|
shops.value = shopList;
|
|
currencies.value = currencyList;
|
|
} catch (error: unknown) {
|
|
errorMessage.value = error instanceof Error ? error.message : t("common.error");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function changePage(nextPage: number): Promise<void> {
|
|
if (nextPage < 1 || nextPage > totalPages.value || nextPage === page.value) return;
|
|
page.value = nextPage;
|
|
await loadOrders();
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadOrders();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<VPage :title="$t('nav.orders')">
|
|
<template #actions>
|
|
<span v-if="!loading" class="text-muted text-sm">{{
|
|
$t("admin.pageOf", { page, total: totalPages })
|
|
}}</span>
|
|
</template>
|
|
<p v-if="loading" class="text-muted text-sm">{{ $t("common.loading") }}</p>
|
|
<p v-else-if="errorMessage" class="text-danger my-2 text-sm" role="alert">{{ errorMessage }}</p>
|
|
<VCard v-else-if="orders.length === 0" class="text-muted">{{ $t("common.empty") }}</VCard>
|
|
<div v-else class="overflow-x-auto">
|
|
<div class="min-w-[900px]">
|
|
<VTable>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t("order.orderNo") }}</th>
|
|
<th>{{ $t("admin.orderShop") }}</th>
|
|
<th>{{ $t("admin.orderUser") }}</th>
|
|
<th>{{ $t("admin.orderTotal") }}</th>
|
|
<th>{{ $t("common.status") }}</th>
|
|
<th>{{ $t("admin.created") }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="order in orders" :key="order.id">
|
|
<td>
|
|
<strong>{{ order.order_no }}</strong>
|
|
</td>
|
|
<td>{{ shopName(order.shop_id) }}</td>
|
|
<td>
|
|
<code class="bg-bg rounded px-1.5 py-0.5" :title="order.user_id">{{
|
|
order.user_id.slice(0, 8)
|
|
}}</code>
|
|
</td>
|
|
<td>{{ formatTotal(order) }}</td>
|
|
<td>
|
|
<VBadge :tone="statusClass(order.status)">{{
|
|
$t(`order.status.${order.status}`)
|
|
}}</VBadge>
|
|
</td>
|
|
<td>{{ formatDate(order.created_at) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</VTable>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="!loading && !errorMessage && orders.length > 0"
|
|
class="mt-4 flex items-center justify-center gap-3"
|
|
>
|
|
<VBtn size="sm" :disabled="page <= 1" @click="changePage(page - 1)">{{
|
|
$t("common.prev")
|
|
}}</VBtn>
|
|
<span class="text-muted text-sm">{{ page }} / {{ totalPages }}</span>
|
|
<VBtn size="sm" :disabled="page >= totalPages" @click="changePage(page + 1)">{{
|
|
$t("common.next")
|
|
}}</VBtn>
|
|
</div>
|
|
</VPage>
|
|
</template>
|