Files
vmall/apps/mall/pages/user/aftersales/apply.vue
T

111 lines
4.8 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 { Aftersale, AftersaleKind, Order } from "@vmall/shared";
definePageMeta({ middleware: "auth" });
const { t } = useI18n();
const { $api } = useNuxtApp();
const route = useRoute();
const { ensureCurrencies, exponentFor } = usePrice();
const order = ref<Order | null>(null);
const aftersales = ref<Aftersale[]>([]);
const loading = ref(true);
const working = ref(false);
const errorKey = ref("");
const kind = ref<AftersaleKind>("refund_only");
const reason = ref("");
const amountMajor = ref("");
const evidence = ref("");
const orderId = computed(() => String(route.query.order_id ?? ""));
const itemId = computed(() => String(route.query.item_id ?? ""));
const item = computed(() => order.value?.items.find((entry) => entry.id === itemId.value) ?? null);
const remainingMinor = computed(() => {
const line = item.value;
if (!line || !order.value) return 0;
const refunded = aftersales.value
.filter((row) => row.order_item_id === line.id && row.status === "refunded")
.reduce((sum, row) => sum + row.amount_minor, 0);
return Math.max(0, line.unit_price_minor * line.qty - refunded);
});
async function load(): Promise<void> {
loading.value = true;
try {
await ensureCurrencies();
const [loadedOrder, loadedAftersales] = await Promise.all([$api.getOrder(orderId.value), $api.listMyAftersales()]);
order.value = loadedOrder;
aftersales.value = loadedAftersales;
} catch {
errorKey.value = "user.aftersaleLoadFailed";
} finally {
loading.value = false;
}
}
function amountMinor(): number | null {
const raw = Number(amountMajor.value);
if (!Number.isFinite(raw) || raw <= 0 || !order.value) return null;
const exponent = exponentFor(order.value.currency);
const minor = Math.round(raw * 10 ** exponent);
return Number.isSafeInteger(minor) && minor > 0 ? minor : null;
}
async function submit(): Promise<void> {
errorKey.value = "";
const content = reason.value.trim();
const amount = amountMinor();
if (!item.value || !order.value || !content || amount === null) {
errorKey.value = "user.validationRequired";
return;
}
if (amount > remainingMinor.value) {
errorKey.value = "user.aftersaleAmountHint";
return;
}
working.value = true;
try {
const created = await $api.applyForAftersale({
order_item_id: item.value.id,
kind: kind.value,
reason: { en: content, zh: content },
amount_minor: amount,
evidence: evidence.value.split("\n").map((url) => url.trim()).filter(Boolean),
});
await navigateTo(`/user/aftersales/${created.id}`);
} catch {
errorKey.value = "user.aftersaleSubmitFailed";
} finally {
working.value = false;
}
}
onMounted(() => void load());
</script>
<template>
<VCard class="min-h-[560px]">
<h1 class="mb-4 text-xl font-bold text-text">{{ t("user.aftersaleApply") }}</h1>
<div v-if="loading" class="py-5 text-muted">{{ t("common.loading") }}</div>
<form v-else class="grid max-w-[640px] gap-4" @submit.prevent="submit">
<VCard :padded="false" class="flex items-center gap-3 border border-border p-4">
<img class="h-14 w-14 border border-border object-contain" :src="item.image || '/mock/product-1.svg'" :alt="item.product_name.en" />
<div class="min-w-0 flex-1"><p class="m-0 truncate text-sm text-text">{{ item.product_name.en }}</p><p class="mt-1 text-xs text-muted">{{ item.sku_code }} × {{ item.qty }}</p></div>
</VCard>
<fieldset class="grid gap-2 border-0 p-0">
<legend class="mb-1 block text-[13px] text-muted">{{ t("user.aftersaleType") }}</legend>
<label class="text-sm text-text"><input v-model="kind" class="mr-1 accent-primary" type="radio" value="refund_only" /> {{ t("user.refundOnly") }}</label>
<label class="text-sm text-text"><input v-model="kind" class="mr-1 accent-primary" type="radio" value="return_refund" /> {{ t("user.returnRefund") }}</label>
</fieldset>
<VField :label="t('user.aftersaleReason')"><textarea v-model="reason" class="min-h-24 rounded-md border border-border bg-surface p-2.5 text-sm text-text" :placeholder="t('user.aftersaleReasonHint')" /></VField>
<VField :label="t('user.aftersaleAmount')">
<VInput v-model="amountMajor" />
<p class="m-0 mt-1 text-xs text-muted">{{ t("user.aftersaleAmountHint") }} {{ t("user.aftersaleRemaining") }}: <PriceText :amount-minor="remainingMinor" :currency="order.currency" /></p>
</VField>
<VField :label="t('user.aftersaleEvidence')"><textarea v-model="evidence" class="min-h-20 rounded-md border border-border bg-surface p-2.5 text-sm text-text" :placeholder="t('user.aftersaleEvidenceHint')" /></VField>
<p v-if="errorKey" class="m-0 text-xs text-danger">{{ t(errorKey) }}</p>
<VBtn class="w-fit" variant="primary" type="submit" :disabled="working">{{ t("user.submitAftersale") }}</VBtn>
</form>
</VCard>
</template>