Files
vmall/apps/admin/pages/orders.vue
T

129 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"]): string {
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>
<div class="page">
<div class="page-head">
<h1 class="page-title">{{ $t("nav.orders") }}</h1>
<span v-if="!loading" class="muted">{{ $t("admin.pageOf", { page, total: totalPages }) }}</span>
</div>
<p v-if="loading" class="muted">{{ $t("common.loading") }}</p>
<p v-else-if="errorMessage" class="error-text" role="alert">{{ errorMessage }}</p>
<div v-else-if="orders.length === 0" class="card muted">{{ $t("common.empty") }}</div>
<div v-else class="table-wrap card">
<table class="table">
<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 :title="order.user_id">{{ order.user_id.slice(0, 8) }}</code></td>
<td>{{ formatTotal(order) }}</td>
<td><span class="badge" :class="statusClass(order.status)">{{ $t(`order.status.${order.status}`) }}</span></td>
<td>{{ formatDate(order.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="!loading && !errorMessage && orders.length > 0" class="pagination">
<button class="btn sm" type="button" :disabled="page <= 1" @click="changePage(page - 1)">
{{ $t("common.prev") }}
</button>
<span class="muted">{{ page }} / {{ totalPages }}</span>
<button class="btn sm" type="button" :disabled="page >= totalPages" @click="changePage(page + 1)">
{{ $t("common.next") }}
</button>
</div>
</div>
</template>
<style scoped>
.table-wrap { overflow-x: auto; padding: 0; }
.table { min-width: 900px; }
.pagination { align-items: center; display: flex; gap: 12px; justify-content: center; margin-top: 16px; }
code { background: #f0f2f5; border-radius: 4px; padding: 2px 5px; }
</style>