wip(freight): migration 0017 + model/column-list groundwork for add-freight-templates
This commit is contained in:
+277
-140
@@ -75,9 +75,7 @@ function currencyExponent(code: string): number | undefined {
|
||||
|
||||
function money(amountMinor: number, currency: string): string {
|
||||
const exponent = currencyExponent(currency);
|
||||
return exponent === undefined
|
||||
? "—"
|
||||
: formatMoney(amountMinor, currency, exponent, locale.value);
|
||||
return exponent === undefined ? "—" : formatMoney(amountMinor, currency, exponent, locale.value);
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
@@ -207,9 +205,8 @@ async function toggleDetail(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function arbitrate(row: Aftersale, outcome: AftersaleArbitration): Promise<void> {
|
||||
const confirmation = outcome === "refund"
|
||||
? t("admin.aftersaleConfirmRefund")
|
||||
: t("admin.aftersaleConfirmReject");
|
||||
const confirmation =
|
||||
outcome === "refund" ? t("admin.aftersaleConfirmRefund") : t("admin.aftersaleConfirmReject");
|
||||
if (!confirm(confirmation)) return;
|
||||
|
||||
acting.value = true;
|
||||
@@ -220,9 +217,10 @@ async function arbitrate(row: Aftersale, outcome: AftersaleArbitration): Promise
|
||||
await loadAftersales();
|
||||
orderCache.delete(row.order_id);
|
||||
await openDetail(row.id);
|
||||
notice.value = outcome === "refund"
|
||||
? t("admin.aftersaleArbitratedRefund")
|
||||
: t("admin.aftersaleArbitratedReject");
|
||||
notice.value =
|
||||
outcome === "refund"
|
||||
? t("admin.aftersaleArbitratedRefund")
|
||||
: t("admin.aftersaleArbitratedReject");
|
||||
} catch (error: unknown) {
|
||||
detailError.value = errorText(error);
|
||||
} finally {
|
||||
@@ -244,7 +242,7 @@ onMounted(() => {
|
||||
<select
|
||||
id="aftersale-status"
|
||||
v-model="statusFilter"
|
||||
class="rounded-md border border-border bg-surface px-3 py-2 font-normal text-text focus:border-primary focus:outline-2 focus:outline-primary/30"
|
||||
class="border-border bg-surface text-text focus:border-primary focus:outline-primary/30 rounded-md border px-3 py-2 font-normal focus:outline-2"
|
||||
@change="loadAftersales"
|
||||
>
|
||||
<option value="">{{ $t("common.all") }}</option>
|
||||
@@ -258,7 +256,7 @@ onMounted(() => {
|
||||
<select
|
||||
id="aftersale-kind"
|
||||
v-model="kindFilter"
|
||||
class="rounded-md border border-border bg-surface px-3 py-2 font-normal text-text focus:border-primary focus:outline-2 focus:outline-primary/30"
|
||||
class="border-border bg-surface text-text focus:border-primary focus:outline-primary/30 rounded-md border px-3 py-2 font-normal focus:outline-2"
|
||||
>
|
||||
<option value="">{{ $t("common.all") }}</option>
|
||||
<option v-for="kind in kinds" :key="kind" :value="kind">{{ kindLabel(kind) }}</option>
|
||||
@@ -276,144 +274,283 @@ onMounted(() => {
|
||||
</div>
|
||||
</VCard>
|
||||
|
||||
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
|
||||
<p v-else-if="errorMessage" class="my-2 text-sm text-danger" role="alert">{{ errorMessage }}</p>
|
||||
<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="filteredAftersales.length === 0" class="text-muted">
|
||||
{{ $t("common.empty") }}
|
||||
</VCard>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<div class="min-w-[1080px]"><VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("admin.aftersaleId") }}</th>
|
||||
<th>{{ $t("admin.aftersaleShop") }}</th>
|
||||
<th>{{ $t("admin.aftersaleBuyer") }}</th>
|
||||
<th>{{ $t("admin.aftersaleKind") }}</th>
|
||||
<th>{{ $t("admin.aftersaleAmount") }}</th>
|
||||
<th>{{ $t("admin.created") }}</th>
|
||||
<th>{{ $t("common.status") }}</th>
|
||||
<th>{{ $t("common.actions") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="row in filteredAftersales" :key="row.id">
|
||||
<tr :class="openId === row.id ? 'bg-primary-soft/30' : ''">
|
||||
<td><code class="rounded bg-bg px-1.5 py-0.5" :title="row.id">{{ shortId(row.id) }}</code></td>
|
||||
<td>
|
||||
<span class="block">{{ shopName(row.shop_id) }}</span>
|
||||
<code class="text-xs text-muted" :title="row.shop_id">{{ shortId(row.shop_id) }}</code>
|
||||
</td>
|
||||
<td><code class="rounded bg-bg px-1.5 py-0.5" :title="row.user_id">{{ shortId(row.user_id) }}</code></td>
|
||||
<td>{{ kindLabel(row.kind) }}</td>
|
||||
<td>{{ money(row.amount_minor, row.currency) }}</td>
|
||||
<td>{{ formatDate(row.created_at) }}</td>
|
||||
<td><VBadge :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</VBadge></td>
|
||||
<td>
|
||||
<VBtn size="sm" @click="toggleDetail(row.id)">
|
||||
{{ openId === row.id ? $t("admin.aftersaleClose") : $t("admin.aftersaleView") }}
|
||||
</VBtn>
|
||||
</td>
|
||||
<div class="min-w-[1080px]">
|
||||
<VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("admin.aftersaleId") }}</th>
|
||||
<th>{{ $t("admin.aftersaleShop") }}</th>
|
||||
<th>{{ $t("admin.aftersaleBuyer") }}</th>
|
||||
<th>{{ $t("admin.aftersaleKind") }}</th>
|
||||
<th>{{ $t("admin.aftersaleAmount") }}</th>
|
||||
<th>{{ $t("admin.created") }}</th>
|
||||
<th>{{ $t("common.status") }}</th>
|
||||
<th>{{ $t("common.actions") }}</th>
|
||||
</tr>
|
||||
<tr v-if="openId === row.id" class="bg-bg">
|
||||
<td colspan="8" class="p-0">
|
||||
<div class="p-4">
|
||||
<p v-if="detailLoading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
|
||||
<p v-else-if="detailError" class="text-sm text-danger" role="alert">{{ detailError }}</p>
|
||||
<VPanel v-else-if="detail" :title="$t('admin.aftersaleDetail')">
|
||||
<template #actions>
|
||||
<VBadge :tone="statusTone(detail.status)">{{ statusLabel(detail.status) }}</VBadge>
|
||||
</template>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleId") }}</span><code class="ml-2">{{ detail.id }}</code></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleOrder") }}</span><code class="ml-2">{{ detail.order_id }}</code></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleFilterShop") }}</span><code class="ml-2">{{ detail.shop_id }}</code></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleFilterBuyer") }}</span><code class="ml-2">{{ detail.user_id }}</code></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleKind") }}</span><span class="ml-2">{{ kindLabel(detail.kind) }}</span></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleCreated") }}</span><span class="ml-2">{{ formatDate(detail.created_at) }}</span></div>
|
||||
<div><span class="text-muted">{{ $t("admin.aftersaleUpdated") }}</span><span class="ml-2">{{ formatDate(detail.updated_at) }}</span></div>
|
||||
<div v-if="detail.reopened"><VBadge tone="orange">{{ $t("admin.aftersaleReopened") }}</VBadge></div>
|
||||
</div>
|
||||
<VCard class="bg-bg" :padded="true">
|
||||
<h4 class="mb-3 text-sm font-semibold">{{ $t("admin.aftersaleOrder") }}</h4>
|
||||
<p v-if="!orderSummary" class="text-sm text-muted">{{ $t("admin.aftersaleOrderNotFound") }}</p>
|
||||
<dl v-else class="grid gap-2 text-sm">
|
||||
<div class="flex justify-between gap-3"><dt class="text-muted">{{ $t("order.orderNo") }}</dt><dd>{{ orderSummary.orderNo }}</dd></div>
|
||||
<div class="flex justify-between gap-3"><dt class="text-muted">{{ $t("admin.aftersaleOrderTotal") }}</dt><dd>{{ money(orderSummary.totalMinor, orderSummary.currency) }}</dd></div>
|
||||
<div class="flex justify-between gap-3 font-semibold"><dt class="text-muted">{{ $t("admin.aftersaleOrderRefunded") }}</dt><dd>{{ money(orderSummary.refundTotalMinor, orderSummary.currency) }}</dd></div>
|
||||
</dl>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<VPanel class="mt-4" :title="$t('admin.aftersaleItem')">
|
||||
<div class="flex gap-3">
|
||||
<img v-if="detail.item.image" :src="detail.item.image" :alt="localizedText(detail.item.product_name, locale)" class="h-16 w-16 rounded object-cover" />
|
||||
<div class="min-w-0 text-sm">
|
||||
<p class="font-medium">{{ localizedText(detail.item.product_name, locale) }}</p>
|
||||
<p class="text-muted">{{ detail.item.sku_code }}</p>
|
||||
<p class="mt-1">{{ money(detail.item.unit_price_minor, detail.currency) }} × {{ detail.item.qty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</VPanel>
|
||||
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<VCard :padded="true">
|
||||
<h4 class="mb-2 text-sm font-semibold">{{ $t("admin.aftersaleReason") }}</h4>
|
||||
<p class="text-sm">{{ localizedText(detail.reason, locale) }}</p>
|
||||
<dl class="mt-3 grid gap-1 text-sm">
|
||||
<div class="flex justify-between gap-3"><dt class="text-muted">{{ $t("admin.aftersaleAmount") }}</dt><dd>{{ money(detail.amount_minor, detail.currency) }}</dd></div>
|
||||
<div class="flex justify-between gap-3"><dt class="text-muted">{{ $t("admin.aftersaleRemaining") }}</dt><dd>{{ money(detail.remaining_refundable_minor, detail.currency) }}</dd></div>
|
||||
</dl>
|
||||
</VCard>
|
||||
<VCard :padded="true">
|
||||
<h4 class="mb-2 text-sm font-semibold">{{ $t("admin.aftersaleEvidence") }}</h4>
|
||||
<ul v-if="detail.evidence.length" class="grid gap-1 text-sm">
|
||||
<li v-for="url in detail.evidence" :key="url"><a :href="url" target="_blank" rel="noopener noreferrer" class="break-all text-primary underline">{{ url }}</a></li>
|
||||
</ul>
|
||||
<p v-else class="text-sm text-muted">{{ $t("admin.aftersaleNoEvidence") }}</p>
|
||||
</VCard>
|
||||
</div>
|
||||
|
||||
<VCard v-if="detail.return_carrier || detail.return_tracking_no" class="mt-4" :padded="true">
|
||||
<h4 class="mb-2 text-sm font-semibold">{{ $t("admin.aftersaleReturnTracking") }}</h4>
|
||||
<p class="text-sm">{{ detail.return_carrier || "—" }} · {{ detail.return_tracking_no || "—" }}</p>
|
||||
</VCard>
|
||||
|
||||
<VPanel class="mt-4" :title="$t('admin.aftersaleMessages')">
|
||||
<div v-if="detail.messages.length" class="grid gap-3">
|
||||
<article v-for="message in [...detail.messages].sort((a, b) => a.created_at.localeCompare(b.created_at))" :key="message.id" class="rounded-md border border-border bg-bg p-3">
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs">
|
||||
<VBadge :tone="roleTone(message.author_role)">{{ roleLabel(message.author_role) }}</VBadge>
|
||||
<code :title="message.author_id">{{ shortId(message.author_id) }}</code>
|
||||
<time class="text-muted">{{ formatDate(message.created_at) }}</time>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="row in filteredAftersales" :key="row.id">
|
||||
<tr :class="openId === row.id ? 'bg-primary-soft/30' : ''">
|
||||
<td>
|
||||
<code class="bg-bg rounded px-1.5 py-0.5" :title="row.id">{{
|
||||
shortId(row.id)
|
||||
}}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="block">{{ shopName(row.shop_id) }}</span>
|
||||
<code class="text-muted text-xs" :title="row.shop_id">{{
|
||||
shortId(row.shop_id)
|
||||
}}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="bg-bg rounded px-1.5 py-0.5" :title="row.user_id">{{
|
||||
shortId(row.user_id)
|
||||
}}</code>
|
||||
</td>
|
||||
<td>{{ kindLabel(row.kind) }}</td>
|
||||
<td>{{ money(row.amount_minor, row.currency) }}</td>
|
||||
<td>{{ formatDate(row.created_at) }}</td>
|
||||
<td>
|
||||
<VBadge :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</VBadge>
|
||||
</td>
|
||||
<td>
|
||||
<VBtn size="sm" @click="toggleDetail(row.id)">
|
||||
{{ openId === row.id ? $t("admin.aftersaleClose") : $t("admin.aftersaleView") }}
|
||||
</VBtn>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="openId === row.id" class="bg-bg">
|
||||
<td colspan="8" class="p-0">
|
||||
<div class="p-4">
|
||||
<p v-if="detailLoading" class="text-muted text-sm">
|
||||
{{ $t("common.loading") }}
|
||||
</p>
|
||||
<p v-else-if="detailError" class="text-danger text-sm" role="alert">
|
||||
{{ detailError }}
|
||||
</p>
|
||||
<VPanel v-else-if="detail" :title="$t('admin.aftersaleDetail')">
|
||||
<template #actions>
|
||||
<VBadge :tone="statusTone(detail.status)">{{
|
||||
statusLabel(detail.status)
|
||||
}}</VBadge>
|
||||
</template>
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<div class="grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleId") }}</span
|
||||
><code class="ml-2">{{ detail.id }}</code>
|
||||
</div>
|
||||
<p class="text-sm">{{ localizedText(message.content, locale) }}</p>
|
||||
<ul v-if="message.evidence.length" class="mt-2 grid gap-1 text-xs">
|
||||
<li v-for="url in message.evidence" :key="url"><a :href="url" target="_blank" rel="noopener noreferrer" class="break-all text-primary underline">{{ url }}</a></li>
|
||||
</ul>
|
||||
</article>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleOrder") }}</span
|
||||
><code class="ml-2">{{ detail.order_id }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleFilterShop") }}</span
|
||||
><code class="ml-2">{{ detail.shop_id }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleFilterBuyer") }}</span
|
||||
><code class="ml-2">{{ detail.user_id }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleKind") }}</span
|
||||
><span class="ml-2">{{ kindLabel(detail.kind) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleCreated") }}</span
|
||||
><span class="ml-2">{{ formatDate(detail.created_at) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted">{{ $t("admin.aftersaleUpdated") }}</span
|
||||
><span class="ml-2">{{ formatDate(detail.updated_at) }}</span>
|
||||
</div>
|
||||
<div v-if="detail.reopened">
|
||||
<VBadge tone="orange">{{ $t("admin.aftersaleReopened") }}</VBadge>
|
||||
</div>
|
||||
</div>
|
||||
<VCard class="bg-bg" :padded="true">
|
||||
<h4 class="mb-3 text-sm font-semibold">
|
||||
{{ $t("admin.aftersaleOrder") }}
|
||||
</h4>
|
||||
<p v-if="!orderSummary" class="text-muted text-sm">
|
||||
{{ $t("admin.aftersaleOrderNotFound") }}
|
||||
</p>
|
||||
<dl v-else class="grid gap-2 text-sm">
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-muted">{{ $t("order.orderNo") }}</dt>
|
||||
<dd>{{ orderSummary.orderNo }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-muted">{{ $t("admin.aftersaleOrderTotal") }}</dt>
|
||||
<dd>{{ money(orderSummary.totalMinor, orderSummary.currency) }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3 font-semibold">
|
||||
<dt class="text-muted">{{ $t("admin.aftersaleOrderRefunded") }}</dt>
|
||||
<dd>
|
||||
{{ money(orderSummary.refundTotalMinor, orderSummary.currency) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</VCard>
|
||||
</div>
|
||||
<p v-else class="text-sm text-muted">{{ $t("admin.aftersaleNoMessages") }}</p>
|
||||
</VPanel>
|
||||
|
||||
<div class="mt-4 border-t border-border pt-4">
|
||||
<p v-if="notice" class="mb-3 text-sm text-success" role="status">{{ notice }}</p>
|
||||
<p v-if="canArbitrate(detail.status)" class="mb-3 text-sm text-muted">{{ $t("admin.aftersaleArbitrate") }}</p>
|
||||
<div v-if="canArbitrate(detail.status)" class="flex flex-wrap gap-2">
|
||||
<VBtn variant="primary" :disabled="acting" @click="arbitrate(detail, 'refund')">{{ $t("admin.aftersaleArbitrateRefund") }}</VBtn>
|
||||
<VBtn variant="danger" :disabled="acting" @click="arbitrate(detail, 'reject')">{{ $t("admin.aftersaleArbitrateReject") }}</VBtn>
|
||||
<VPanel class="mt-4" :title="$t('admin.aftersaleItem')">
|
||||
<div class="flex gap-3">
|
||||
<img
|
||||
v-if="detail.item.image"
|
||||
:src="detail.item.image"
|
||||
:alt="localizedText(detail.item.product_name, locale)"
|
||||
class="h-16 w-16 rounded object-cover"
|
||||
/>
|
||||
<div class="min-w-0 text-sm">
|
||||
<p class="font-medium">
|
||||
{{ localizedText(detail.item.product_name, locale) }}
|
||||
</p>
|
||||
<p class="text-muted">{{ detail.item.sku_code }}</p>
|
||||
<p class="mt-1">
|
||||
{{ money(detail.item.unit_price_minor, detail.currency) }} ×
|
||||
{{ detail.item.qty }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</VPanel>
|
||||
|
||||
<div class="mt-4 grid gap-4 lg:grid-cols-2">
|
||||
<VCard :padded="true">
|
||||
<h4 class="mb-2 text-sm font-semibold">
|
||||
{{ $t("admin.aftersaleReason") }}
|
||||
</h4>
|
||||
<p class="text-sm">{{ localizedText(detail.reason, locale) }}</p>
|
||||
<dl class="mt-3 grid gap-1 text-sm">
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-muted">{{ $t("admin.aftersaleAmount") }}</dt>
|
||||
<dd>{{ money(detail.amount_minor, detail.currency) }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-3">
|
||||
<dt class="text-muted">{{ $t("admin.aftersaleRemaining") }}</dt>
|
||||
<dd>
|
||||
{{ money(detail.remaining_refundable_minor, detail.currency) }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</VCard>
|
||||
<VCard :padded="true">
|
||||
<h4 class="mb-2 text-sm font-semibold">
|
||||
{{ $t("admin.aftersaleEvidence") }}
|
||||
</h4>
|
||||
<ul v-if="detail.evidence.length" class="grid gap-1 text-sm">
|
||||
<li v-for="url in detail.evidence" :key="url">
|
||||
<a
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary break-all underline"
|
||||
>{{ url }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-muted text-sm">
|
||||
{{ $t("admin.aftersaleNoEvidence") }}
|
||||
</p>
|
||||
</VCard>
|
||||
</div>
|
||||
<p v-else-if="detail.status === 'refunded'" class="text-sm text-success">{{ $t("admin.aftersaleOutcomeRefunded") }}</p>
|
||||
<p v-else-if="detail.status === 'rejected'" class="text-sm text-danger">{{ $t("admin.aftersaleOutcomeRejected") }}</p>
|
||||
<p v-else class="text-sm text-muted">{{ $t("admin.aftersaleOutcomeCancelled") }}</p>
|
||||
</div>
|
||||
</VPanel>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</VTable></div>
|
||||
|
||||
<VCard
|
||||
v-if="detail.return_carrier || detail.return_tracking_no"
|
||||
class="mt-4"
|
||||
:padded="true"
|
||||
>
|
||||
<h4 class="mb-2 text-sm font-semibold">
|
||||
{{ $t("admin.aftersaleReturnTracking") }}
|
||||
</h4>
|
||||
<p class="text-sm">
|
||||
{{ detail.return_carrier || "—" }} ·
|
||||
{{ detail.return_tracking_no || "—" }}
|
||||
</p>
|
||||
</VCard>
|
||||
|
||||
<VPanel class="mt-4" :title="$t('admin.aftersaleMessages')">
|
||||
<div v-if="detail.messages.length" class="grid gap-3">
|
||||
<article
|
||||
v-for="message in [...detail.messages].sort((a, b) =>
|
||||
a.created_at.localeCompare(b.created_at),
|
||||
)"
|
||||
:key="message.id"
|
||||
class="border-border bg-bg rounded-md border p-3"
|
||||
>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2 text-xs">
|
||||
<VBadge :tone="roleTone(message.author_role)">{{
|
||||
roleLabel(message.author_role)
|
||||
}}</VBadge>
|
||||
<code :title="message.author_id">{{
|
||||
shortId(message.author_id)
|
||||
}}</code>
|
||||
<time class="text-muted">{{ formatDate(message.created_at) }}</time>
|
||||
</div>
|
||||
<p class="text-sm">{{ localizedText(message.content, locale) }}</p>
|
||||
<ul v-if="message.evidence.length" class="mt-2 grid gap-1 text-xs">
|
||||
<li v-for="url in message.evidence" :key="url">
|
||||
<a
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary break-all underline"
|
||||
>{{ url }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<p v-else class="text-muted text-sm">
|
||||
{{ $t("admin.aftersaleNoMessages") }}
|
||||
</p>
|
||||
</VPanel>
|
||||
|
||||
<div class="border-border mt-4 border-t pt-4">
|
||||
<p v-if="notice" class="text-success mb-3 text-sm" role="status">
|
||||
{{ notice }}
|
||||
</p>
|
||||
<p v-if="canArbitrate(detail.status)" class="text-muted mb-3 text-sm">
|
||||
{{ $t("admin.aftersaleArbitrate") }}
|
||||
</p>
|
||||
<div v-if="canArbitrate(detail.status)" class="flex flex-wrap gap-2">
|
||||
<VBtn
|
||||
variant="primary"
|
||||
:disabled="acting"
|
||||
@click="arbitrate(detail, 'refund')"
|
||||
>{{ $t("admin.aftersaleArbitrateRefund") }}</VBtn
|
||||
>
|
||||
<VBtn
|
||||
variant="danger"
|
||||
:disabled="acting"
|
||||
@click="arbitrate(detail, 'reject')"
|
||||
>{{ $t("admin.aftersaleArbitrateReject") }}</VBtn
|
||||
>
|
||||
</div>
|
||||
<p v-else-if="detail.status === 'refunded'" class="text-success text-sm">
|
||||
{{ $t("admin.aftersaleOutcomeRefunded") }}
|
||||
</p>
|
||||
<p v-else-if="detail.status === 'rejected'" class="text-danger text-sm">
|
||||
{{ $t("admin.aftersaleOutcomeRejected") }}
|
||||
</p>
|
||||
<p v-else class="text-muted text-sm">
|
||||
{{ $t("admin.aftersaleOutcomeCancelled") }}
|
||||
</p>
|
||||
</div>
|
||||
</VPanel>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</div>
|
||||
</div>
|
||||
</VPage>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user