The product page loads a shop's claimable coupons through the shared client and claims them in place; the buyer coupon list reads owned snapshots with their claimed/redeemed/expired state. Checkout offers each shop order at most one owned coupon and submits only the choice; the pay page and order detail render the server-persisted discount and reduced total, never a client figure. Coupon fixtures leave the pages; the fixed-data adapter still serves the coupon domain as the rollback path. The demo seed creates two deterministic templates and claims them for the demo customer.
79 lines
2.1 KiB
Vue
79 lines
2.1 KiB
Vue
<script setup lang="ts">
|
|
import { t as pick } from "@vmall/shared";
|
|
import type { Coupon } from "@vmall/shared";
|
|
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const { locale, t } = useI18n();
|
|
const { $api } = useNuxtApp();
|
|
const coupons = ref<Coupon[]>([]);
|
|
const loading = ref(true);
|
|
const error = ref("");
|
|
|
|
const statusLabels = computed<Record<string, string>>(() => ({
|
|
claimed: t("user.couponClaimed"),
|
|
redeemed: t("user.couponRedeemed"),
|
|
expired: t("user.couponExpired"),
|
|
}));
|
|
|
|
async function load(): Promise<void> {
|
|
loading.value = true;
|
|
error.value = "";
|
|
try {
|
|
coupons.value = await $api.listMyCoupons();
|
|
} catch {
|
|
error.value = t("user.loadFailed");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => void load());
|
|
</script>
|
|
|
|
<template>
|
|
<section class="mpanel coupons-panel">
|
|
<h1 class="mpanel-title">{{ t("user.couponsTitle") }}</h1>
|
|
<p v-if="error" class="error-message">{{ error }}</p>
|
|
<div v-else-if="loading" class="loading-state">{{ t("common.loading") }}</div>
|
|
<UiEmptyState v-else-if="coupons.length === 0" :text="t('user.noCoupons')" />
|
|
<table v-else class="mtable">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ t("user.couponTitle") }}</th>
|
|
<th>{{ t("user.amount") }}</th>
|
|
<th>{{ t("user.threshold") }}</th>
|
|
<th>{{ t("user.expiresAt") }}</th>
|
|
<th>{{ t("user.couponStatus") }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="coupon in coupons" :key="coupon.id">
|
|
<td>{{ pick(coupon.title, locale) }}</td>
|
|
<td><PriceText :amount-minor="coupon.amount_minor" :currency="coupon.currency" /></td>
|
|
<td><PriceText :amount-minor="coupon.threshold_minor" :currency="coupon.currency" /></td>
|
|
<td>{{ coupon.ends_at.slice(0, 10) }}</td>
|
|
<td>{{ statusLabels[coupon.status] ?? coupon.status }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.coupons-panel {
|
|
min-height: 560px;
|
|
}
|
|
.error-message {
|
|
margin: 0 0 16px;
|
|
padding: 10px 14px;
|
|
color: #b42318;
|
|
background: #fff1f0;
|
|
border: 1px solid #ffd6d2;
|
|
}
|
|
.loading-state {
|
|
padding: 40px 0;
|
|
color: var(--mall-muted);
|
|
}
|
|
</style>
|