Implements, verifies, and archives the three remaining Wave 2 changes from openspec/MIGRATION-PLAN.md. - add-wallet-settlement (P3): demo recharge, guarded withdrawal freeze and one-time admin review, paginated own fund entries, idempotent per-shop weekly/monthly settlement statements with commission rate and one-time payout confirmation. - add-merchant-onboarding (P5): personal/enterprise applications with one live application per user, guarded review with mandatory rejection reason, and transactional shop + owner provisioning returning one-time credentials; mall onboarding/status pages and an admin review console. - add-membership-messaging (P7): platform member levels, append-only growth accrual on order completion with guarded one-way leveling, order/shipment/ refund system messages with unread/read state and soft deletion, plus the mall header unread badge. Backend: migrations 0019-0023, new wallet, settlement, merchant_onboarding, membership and messaging modules, event hooks in order/fulfillment/aftersale, and integration suites for each. Shared contract extended and all three frontends updated; code indexes, domain docs, backend guidelines and the migration tracker synced. Verification: cargo test -p vmall-api green twice consecutively; mall, admin and shop-admin builds pass; browser smoke on every new surface; openspec validate --all --strict green (33 passed). The three changes share the @vmall/shared contract, the mall mock adapter and per-app locale/nav files, so they are committed together to keep every commit buildable.
609 lines
25 KiB
Vue
609 lines
25 KiB
Vue
<script setup lang="ts">
|
|
import { ApiError, t as localizedText } from "@vmall/shared";
|
|
import type {
|
|
MerchantApplication,
|
|
MerchantApplicationStatus,
|
|
MerchantEntityType,
|
|
MerchantOwnerCredentials,
|
|
} from "@vmall/shared";
|
|
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const { $api } = useNuxtApp();
|
|
const { locale, t } = useI18n();
|
|
|
|
const applications = ref<MerchantApplication[]>([]);
|
|
const statusFilter = ref<"" | MerchantApplicationStatus>("");
|
|
const page = ref(1);
|
|
const total = ref(0);
|
|
const perPage = ref(20);
|
|
const loading = ref(true);
|
|
const errorMessage = ref("");
|
|
const notice = ref("");
|
|
const conflictMessage = ref("");
|
|
const validationMessage = ref("");
|
|
|
|
const openId = ref<string | null>(null);
|
|
const detail = ref<MerchantApplication | null>(null);
|
|
const detailLoading = ref(false);
|
|
const detailError = ref("");
|
|
|
|
const acting = ref(false);
|
|
const rejectReason = ref("");
|
|
const rejectError = ref("");
|
|
|
|
const credentials = ref<MerchantOwnerCredentials | null>(null);
|
|
|
|
const statuses: MerchantApplicationStatus[] = ["pending", "approved", "rejected"];
|
|
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / perPage.value)));
|
|
|
|
function statusTone(status: MerchantApplicationStatus): "green" | "red" | "orange" {
|
|
if (status === "approved") return "green";
|
|
if (status === "rejected") return "red";
|
|
return "orange";
|
|
}
|
|
|
|
function statusLabel(status: MerchantApplicationStatus): string {
|
|
return t(`admin.merchantStatuses.${status}`);
|
|
}
|
|
|
|
function entityTypeLabel(entityType: MerchantEntityType): string {
|
|
return t(`admin.merchantEntityTypes.${entityType}`);
|
|
}
|
|
|
|
function entityName(row: MerchantApplication): string {
|
|
const name = row.entity_type === "personal" ? row.real_name : row.company_name;
|
|
return name && name.trim() !== "" ? name : "—";
|
|
}
|
|
|
|
function formatDate(value: string): string {
|
|
return new Date(value).toLocaleString(locale.value === "zh" ? "zh-CN" : "en-US");
|
|
}
|
|
|
|
function shortId(value: string): string {
|
|
return value.slice(0, 8);
|
|
}
|
|
|
|
function extraMaterials(row: MerchantApplication): string[] {
|
|
return row.qualification.extra_materials ?? [];
|
|
}
|
|
|
|
function hasQualification(row: MerchantApplication): boolean {
|
|
const qualification = row.qualification;
|
|
return Boolean(
|
|
qualification.identity_document_url ||
|
|
qualification.business_license_url ||
|
|
qualification.business_license_no ||
|
|
extraMaterials(row).length > 0,
|
|
);
|
|
}
|
|
|
|
function errorText(error: unknown): string {
|
|
const message = error instanceof Error ? error.message : t("common.error");
|
|
return error instanceof ApiError && error.status === 409
|
|
? `${t("admin.merchantConflict")}: ${message}`
|
|
: message;
|
|
}
|
|
|
|
function clearFeedback(): void {
|
|
errorMessage.value = "";
|
|
notice.value = "";
|
|
conflictMessage.value = "";
|
|
validationMessage.value = "";
|
|
}
|
|
|
|
function closeDetail(): void {
|
|
openId.value = null;
|
|
detail.value = null;
|
|
detailError.value = "";
|
|
rejectReason.value = "";
|
|
rejectError.value = "";
|
|
}
|
|
|
|
async function load(): Promise<void> {
|
|
loading.value = true;
|
|
errorMessage.value = "";
|
|
try {
|
|
const paged = await $api.admin.listMerchantApplications({
|
|
status: statusFilter.value || undefined,
|
|
page: page.value,
|
|
per_page: perPage.value,
|
|
});
|
|
if (paged.items.length === 0 && paged.total > 0 && page.value > 1) {
|
|
page.value -= 1;
|
|
await load();
|
|
return;
|
|
}
|
|
applications.value = paged.items;
|
|
total.value = paged.total;
|
|
perPage.value = paged.per_page;
|
|
if (openId.value && !paged.items.some((row) => row.id === openId.value)) {
|
|
closeDetail();
|
|
}
|
|
} 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 load();
|
|
}
|
|
|
|
async function applyStatusFilter(): Promise<void> {
|
|
page.value = 1;
|
|
closeDetail();
|
|
await load();
|
|
}
|
|
|
|
async function openDetail(id: string): Promise<void> {
|
|
openId.value = id;
|
|
detail.value = null;
|
|
detailError.value = "";
|
|
rejectReason.value = "";
|
|
rejectError.value = "";
|
|
validationMessage.value = "";
|
|
notice.value = "";
|
|
detailLoading.value = true;
|
|
try {
|
|
detail.value = await $api.admin.getMerchantApplication(id);
|
|
} catch (error: unknown) {
|
|
detailError.value = errorText(error);
|
|
} finally {
|
|
detailLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function toggleDetail(id: string): Promise<void> {
|
|
if (openId.value === id) {
|
|
closeDetail();
|
|
return;
|
|
}
|
|
await openDetail(id);
|
|
}
|
|
|
|
function dismissCredentials(): void {
|
|
credentials.value = null;
|
|
}
|
|
|
|
async function approve(row: MerchantApplication): Promise<void> {
|
|
if (!confirm(t("admin.merchantConfirmApprove"))) return;
|
|
|
|
acting.value = true;
|
|
clearFeedback();
|
|
rejectError.value = "";
|
|
try {
|
|
const result = await $api.admin.approveMerchantApplication(row.id);
|
|
detail.value = result.application;
|
|
// The initial password exists only in this response: show it once, then discard.
|
|
credentials.value = result.credentials;
|
|
notice.value = t("admin.merchantApprovedNotice");
|
|
} catch (error: unknown) {
|
|
if (error instanceof ApiError && error.status === 409) {
|
|
// Already reviewed: converge to server state instead of retrying.
|
|
conflictMessage.value = errorText(error);
|
|
closeDetail();
|
|
} else if (error instanceof ApiError && error.status === 400) {
|
|
validationMessage.value = error instanceof Error ? error.message : t("common.error");
|
|
} else {
|
|
detailError.value = error instanceof Error ? error.message : t("common.error");
|
|
return;
|
|
}
|
|
} finally {
|
|
acting.value = false;
|
|
}
|
|
await load();
|
|
}
|
|
|
|
async function reject(row: MerchantApplication): Promise<void> {
|
|
const reason = rejectReason.value.trim();
|
|
rejectError.value = "";
|
|
validationMessage.value = "";
|
|
// The server rejects a blank reason with 400; block it before the request too.
|
|
if (reason === "") {
|
|
rejectError.value = t("admin.merchantRejectReasonRequired");
|
|
return;
|
|
}
|
|
if (!confirm(t("admin.merchantConfirmReject"))) return;
|
|
|
|
acting.value = true;
|
|
clearFeedback();
|
|
try {
|
|
detail.value = await $api.admin.rejectMerchantApplication(row.id, reason);
|
|
rejectReason.value = "";
|
|
notice.value = t("admin.merchantRejectedNotice");
|
|
} catch (error: unknown) {
|
|
if (error instanceof ApiError && error.status === 409) {
|
|
conflictMessage.value = errorText(error);
|
|
closeDetail();
|
|
} else if (error instanceof ApiError && error.status === 400) {
|
|
validationMessage.value = error instanceof Error ? error.message : t("common.error");
|
|
} else {
|
|
detailError.value = error instanceof Error ? error.message : t("common.error");
|
|
return;
|
|
}
|
|
} finally {
|
|
acting.value = false;
|
|
}
|
|
await load();
|
|
}
|
|
|
|
onMounted(() => {
|
|
void load();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<VPage :title="$t('nav.merchantApplications')">
|
|
<template #actions>
|
|
<span v-if="!loading" class="text-muted text-sm">{{ total }}</span>
|
|
</template>
|
|
|
|
<VCard class="mb-5">
|
|
<div class="flex flex-wrap items-end gap-3">
|
|
<label class="grid gap-1 text-sm font-medium" for="merchant-application-status">
|
|
{{ $t("common.status") }}
|
|
<select
|
|
id="merchant-application-status"
|
|
v-model="statusFilter"
|
|
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="applyStatusFilter"
|
|
>
|
|
<option value="">{{ $t("common.all") }}</option>
|
|
<option v-for="status in statuses" :key="status" :value="status">
|
|
{{ statusLabel(status) }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<VBtn size="sm" :disabled="loading" @click="load">{{ $t("admin.refresh") }}</VBtn>
|
|
</div>
|
|
</VCard>
|
|
|
|
<p v-if="notice" class="text-success my-2 text-sm" role="status">{{ notice }}</p>
|
|
<p
|
|
v-if="conflictMessage"
|
|
class="bg-warning/10 text-warning my-2 rounded-md px-3 py-2 text-sm"
|
|
role="alert"
|
|
>
|
|
{{ conflictMessage }}
|
|
</p>
|
|
<p v-if="validationMessage" class="text-danger my-2 text-sm" role="alert">
|
|
{{ validationMessage }}
|
|
</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="applications.length === 0" class="text-muted">
|
|
{{ $t("common.empty") }}
|
|
</VCard>
|
|
<div v-else class="overflow-x-auto">
|
|
<div class="min-w-[1040px]">
|
|
<VTable>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t("admin.merchantApplication") }}</th>
|
|
<th>{{ $t("admin.merchantApplicant") }}</th>
|
|
<th>{{ $t("admin.merchantEntityType") }}</th>
|
|
<th>{{ $t("admin.merchantEntityName") }}</th>
|
|
<th>{{ $t("admin.merchantSubmittedAt") }}</th>
|
|
<th>{{ $t("common.status") }}</th>
|
|
<th>{{ $t("common.actions") }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="row in applications" :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>{{ row.applicant_email }}</td>
|
|
<td>
|
|
<VBadge :tone="row.entity_type === 'enterprise' ? 'blue' : 'gray'">
|
|
{{ entityTypeLabel(row.entity_type) }}
|
|
</VBadge>
|
|
</td>
|
|
<td>{{ entityName(row) }}</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.merchantClose") : $t("admin.merchantView")
|
|
}}
|
|
</VBtn>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="openId === row.id" class="bg-bg">
|
|
<td colspan="7" 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.merchantDetail')">
|
|
<template #actions>
|
|
<VBadge :tone="statusTone(detail.status)">{{
|
|
statusLabel(detail.status)
|
|
}}</VBadge>
|
|
<VBtn size="sm" class="ml-2" @click="closeDetail">{{
|
|
$t("admin.merchantClose")
|
|
}}</VBtn>
|
|
</template>
|
|
|
|
<dl class="mb-4 grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantApplication") }}</dt>
|
|
<dd><code class="break-all">{{ detail.id }}</code></dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantApplicant") }}</dt>
|
|
<dd>{{ detail.applicant_email }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.orderUser") }}</dt>
|
|
<dd><code class="break-all">{{ detail.user_id }}</code></dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantEntityType") }}</dt>
|
|
<dd>{{ entityTypeLabel(detail.entity_type) }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantSubmittedAt") }}</dt>
|
|
<dd>{{ formatDate(detail.created_at) }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantUpdated") }}</dt>
|
|
<dd>{{ formatDate(detail.updated_at) }}</dd>
|
|
</div>
|
|
<div>
|
|
<dt class="text-muted">{{ $t("admin.merchantReviewedAt") }}</dt>
|
|
<dd>
|
|
<span v-if="detail.reviewed_at">{{ formatDate(detail.reviewed_at) }}</span>
|
|
<span v-else class="text-muted">—</span>
|
|
</dd>
|
|
</div>
|
|
<div v-if="detail.created_shop_id">
|
|
<dt class="text-muted">{{ $t("admin.merchantCreatedShop") }}</dt>
|
|
<dd><code class="break-all">{{ detail.created_shop_id }}</code></dd>
|
|
</div>
|
|
</dl>
|
|
|
|
<div class="grid gap-4 lg:grid-cols-2">
|
|
<VCard :padded="true">
|
|
<h4 class="mb-2 text-sm font-semibold">
|
|
{{ $t("admin.merchantEntityInfo") }}
|
|
</h4>
|
|
<dl class="grid gap-1 text-sm">
|
|
<template v-if="detail.entity_type === 'personal'">
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantRealName") }}</dt>
|
|
<dd>{{ detail.real_name || "—" }}</dd>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantCompanyName") }}</dt>
|
|
<dd>{{ detail.company_name || "—" }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">
|
|
{{ $t("admin.merchantBusinessLicenseNo") }}
|
|
</dt>
|
|
<dd>{{ detail.business_license_no || "—" }}</dd>
|
|
</div>
|
|
</template>
|
|
</dl>
|
|
</VCard>
|
|
|
|
<VCard :padded="true">
|
|
<h4 class="mb-2 text-sm font-semibold">
|
|
{{ $t("admin.merchantContact") }}
|
|
</h4>
|
|
<dl class="grid gap-1 text-sm">
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantContactName") }}</dt>
|
|
<dd>{{ detail.contact.name }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantContactPhone") }}</dt>
|
|
<dd>{{ detail.contact.phone }}</dd>
|
|
</div>
|
|
<div class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantContactEmail") }}</dt>
|
|
<dd class="break-all">{{ detail.contact.email }}</dd>
|
|
</div>
|
|
<div v-if="detail.contact.address" class="flex justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantContactAddress") }}</dt>
|
|
<dd>{{ detail.contact.address }}</dd>
|
|
</div>
|
|
</dl>
|
|
</VCard>
|
|
|
|
<VCard :padded="true">
|
|
<h4 class="mb-2 text-sm font-semibold">
|
|
{{ $t("admin.merchantCategories") }}
|
|
</h4>
|
|
<ul v-if="detail.categories.length" class="flex flex-wrap gap-2 text-sm">
|
|
<li v-for="category in detail.categories" :key="category.id">
|
|
<VBadge tone="gray">{{
|
|
localizedText(category.name, locale)
|
|
}}</VBadge>
|
|
</li>
|
|
</ul>
|
|
<p v-else class="text-muted text-sm">
|
|
{{ $t("admin.merchantNoCategories") }}
|
|
</p>
|
|
</VCard>
|
|
|
|
<VCard :padded="true">
|
|
<h4 class="mb-2 text-sm font-semibold">
|
|
{{ $t("admin.merchantQualification") }}
|
|
</h4>
|
|
<div v-if="hasQualification(detail)" class="grid gap-2 text-sm">
|
|
<div v-if="detail.qualification.identity_document_url">
|
|
<span class="text-muted">{{ $t("admin.merchantIdentityDocument") }}</span>
|
|
<a
|
|
:href="detail.qualification.identity_document_url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-primary block break-all underline"
|
|
>{{ detail.qualification.identity_document_url }}</a
|
|
>
|
|
</div>
|
|
<div v-if="detail.qualification.business_license_url">
|
|
<span class="text-muted">{{ $t("admin.merchantBusinessLicense") }}</span>
|
|
<a
|
|
:href="detail.qualification.business_license_url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-primary block break-all underline"
|
|
>{{ detail.qualification.business_license_url }}</a
|
|
>
|
|
</div>
|
|
<div v-if="detail.qualification.business_license_no">
|
|
<span class="text-muted">
|
|
{{ $t("admin.merchantBusinessLicenseNo") }}
|
|
</span>
|
|
<span class="block">{{ detail.qualification.business_license_no }}</span>
|
|
</div>
|
|
<div v-if="extraMaterials(detail).length">
|
|
<span class="text-muted">{{ $t("admin.merchantExtraMaterials") }}</span>
|
|
<ul class="grid gap-1">
|
|
<li v-for="url in extraMaterials(detail)" :key="url">
|
|
<a
|
|
:href="url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-primary break-all underline"
|
|
>{{ url }}</a
|
|
>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-muted text-sm">
|
|
{{ $t("admin.merchantNoQualification") }}
|
|
</p>
|
|
</VCard>
|
|
</div>
|
|
|
|
<p
|
|
v-if="detail.status === 'rejected'"
|
|
class="bg-danger/10 text-danger mt-4 rounded-md px-3 py-2 text-sm"
|
|
role="alert"
|
|
>
|
|
<span class="font-medium">{{ $t("admin.merchantRejectionReason") }}</span>
|
|
{{ detail.rejection_reason || "—" }}
|
|
</p>
|
|
|
|
<div v-if="detail.status === 'pending'" class="border-border mt-4 border-t pt-4">
|
|
<p class="text-muted mb-3 text-sm">
|
|
{{ $t("admin.merchantConfirmApprove") }}
|
|
</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<VBtn variant="primary" :disabled="acting" @click="approve(detail)">
|
|
{{ acting ? $t("common.loading") : $t("admin.merchantApprove") }}
|
|
</VBtn>
|
|
</div>
|
|
<div class="mt-4 max-w-md">
|
|
<VField
|
|
:label="$t('admin.merchantRejectReason')"
|
|
:error="rejectError"
|
|
>
|
|
<VInput
|
|
id="merchant-reject-reason"
|
|
v-model="rejectReason"
|
|
:placeholder="$t('admin.merchantRejectReasonPlaceholder')"
|
|
:disabled="acting"
|
|
/>
|
|
</VField>
|
|
<VBtn variant="danger" :disabled="acting" @click="reject(detail)">
|
|
{{ $t("admin.merchantReject") }}
|
|
</VBtn>
|
|
</div>
|
|
</div>
|
|
<p v-else-if="detail.status === 'approved'" class="text-success mt-4 text-sm">
|
|
{{ $t("admin.merchantOutcomeApproved") }}
|
|
</p>
|
|
<p v-else class="text-danger mt-4 text-sm">
|
|
{{ $t("admin.merchantOutcomeRejected") }}
|
|
</p>
|
|
</VPanel>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</VTable>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!loading && !errorMessage && applications.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>
|
|
|
|
<div
|
|
v-if="credentials"
|
|
class="bg-text/40 fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="merchant-credentials-heading"
|
|
>
|
|
<VCard class="max-h-full w-full max-w-lg overflow-y-auto">
|
|
<h2 id="merchant-credentials-heading" class="text-text mb-2 text-base font-semibold">
|
|
{{ $t("admin.merchantCredentialsTitle") }}
|
|
</h2>
|
|
<p class="bg-warning/10 text-warning mb-4 rounded-md px-3 py-2 text-sm" role="alert">
|
|
{{ $t("admin.merchantCredentialsWarning") }}
|
|
</p>
|
|
<dl class="grid gap-2 text-sm">
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantCredentialsEmail") }}</dt>
|
|
<dd><code class="bg-bg rounded px-1.5 py-0.5 break-all">{{ credentials.email }}</code></dd>
|
|
</div>
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantCredentialsPassword") }}</dt>
|
|
<dd>
|
|
<code class="bg-bg rounded px-1.5 py-0.5 break-all">{{
|
|
credentials.initial_password
|
|
}}</code>
|
|
</dd>
|
|
</div>
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantCredentialsShopSlug") }}</dt>
|
|
<dd><code class="bg-bg rounded px-1.5 py-0.5 break-all">{{ credentials.shop_slug }}</code></dd>
|
|
</div>
|
|
<div class="flex flex-wrap justify-between gap-3">
|
|
<dt class="text-muted">{{ $t("admin.merchantCredentialsShopId") }}</dt>
|
|
<dd><code class="bg-bg rounded px-1.5 py-0.5 break-all">{{ credentials.shop_id }}</code></dd>
|
|
</div>
|
|
</dl>
|
|
<div class="mt-5 flex justify-end">
|
|
<VBtn variant="primary" @click="dismissCredentials">
|
|
{{ $t("admin.merchantCredentialsClose") }}
|
|
</VBtn>
|
|
</div>
|
|
</VCard>
|
|
</div>
|
|
</VPage>
|
|
</template>
|