feat(admin,shop-admin): content/brand management + merchant shop profile (add-content-admin-ui)
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import type { ShopProfileSelfInput } from "@vmall/shared";
|
||||
|
||||
definePageMeta({ middleware: "auth" });
|
||||
|
||||
const { $api } = useNuxtApp();
|
||||
const { t } = useI18n();
|
||||
|
||||
const form = reactive({
|
||||
logo: "",
|
||||
banner: "",
|
||||
company: "",
|
||||
region: "",
|
||||
addressEn: "",
|
||||
addressZh: "",
|
||||
noticeEn: "",
|
||||
noticeZh: "",
|
||||
afterSaleEn: "",
|
||||
afterSaleZh: "",
|
||||
});
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const loadError = ref("");
|
||||
const formError = ref("");
|
||||
const feedback = ref<{ ok: boolean; message: string } | null>(null);
|
||||
|
||||
async function loadProfile(): Promise<void> {
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
const shop = await $api.shop.getMyShop();
|
||||
const profile = await $api.getShop(shop.slug);
|
||||
form.logo = profile.logo ?? "";
|
||||
form.banner = profile.banner ?? "";
|
||||
form.company = profile.company ?? "";
|
||||
form.region = profile.region ?? "";
|
||||
form.addressEn = profile.address?.en ?? "";
|
||||
form.addressZh = profile.address?.zh ?? "";
|
||||
form.noticeEn = profile.notice?.en ?? "";
|
||||
form.noticeZh = profile.notice?.zh ?? "";
|
||||
form.afterSaleEn = profile.after_sale?.en ?? "";
|
||||
form.afterSaleZh = profile.after_sale?.zh ?? "";
|
||||
} catch (error: unknown) {
|
||||
loadError.value = error instanceof Error ? error.message : t("common.error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bilingualField(en: string, zh: string): { en: string; zh: string } | null {
|
||||
const enText = en.trim();
|
||||
const zhText = zh.trim();
|
||||
if (!enText && !zhText) return null;
|
||||
if (!enText || !zhText) throw new Error(t("shop.bilingualRequired"));
|
||||
return { en: enText, zh: zhText };
|
||||
}
|
||||
|
||||
async function save(): Promise<void> {
|
||||
feedback.value = null;
|
||||
formError.value = "";
|
||||
let body: ShopProfileSelfInput;
|
||||
try {
|
||||
body = {
|
||||
logo: form.logo.trim() || null,
|
||||
banner: form.banner.trim() || null,
|
||||
company: form.company.trim() || null,
|
||||
region: form.region.trim() || null,
|
||||
address: bilingualField(form.addressEn, form.addressZh),
|
||||
notice: bilingualField(form.noticeEn, form.noticeZh),
|
||||
after_sale: bilingualField(form.afterSaleEn, form.afterSaleZh),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
formError.value = error instanceof Error ? error.message : t("common.error");
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
await $api.shop.updateMyProfile(body);
|
||||
feedback.value = { ok: true, message: t("shop.profileSaved") };
|
||||
} catch (error: unknown) {
|
||||
feedback.value = {
|
||||
ok: false,
|
||||
message: error instanceof Error ? error.message : t("common.error"),
|
||||
};
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-md border border-border bg-surface px-3 py-2 text-sm text-text focus:border-primary focus:outline-2 focus:outline-primary/30 disabled:opacity-50";
|
||||
|
||||
onMounted(() => {
|
||||
void loadProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPage :title="$t('nav.shopProfile')">
|
||||
<p v-if="loading" class="text-sm text-muted">{{ $t("common.loading") }}</p>
|
||||
<p v-else-if="loadError" class="my-2 text-sm text-danger" role="alert">{{ loadError }}</p>
|
||||
<VCard v-else>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<VField :label="$t('shop.profileLogo')">
|
||||
<VInput v-model="form.logo" placeholder="https://…" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileBanner')">
|
||||
<VInput v-model="form.banner" placeholder="https://…" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileCompany')">
|
||||
<VInput v-model="form.company" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileRegion')">
|
||||
<VInput v-model="form.region" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileAddressEn')">
|
||||
<VInput v-model="form.addressEn" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileAddressZh')">
|
||||
<VInput v-model="form.addressZh" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileNoticeEn')">
|
||||
<textarea v-model="form.noticeEn" :class="inputClass" rows="3" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileNoticeZh')">
|
||||
<textarea v-model="form.noticeZh" :class="inputClass" rows="3" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileAfterSaleEn')">
|
||||
<textarea v-model="form.afterSaleEn" :class="inputClass" rows="3" />
|
||||
</VField>
|
||||
<VField :label="$t('shop.profileAfterSaleZh')">
|
||||
<textarea v-model="form.afterSaleZh" :class="inputClass" rows="3" />
|
||||
</VField>
|
||||
</div>
|
||||
<p v-if="formError" class="my-2 text-sm text-danger" role="alert">{{ formError }}</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<VBtn variant="primary" :disabled="saving" @click="save">
|
||||
{{ saving ? $t("common.loading") : $t("common.save") }}
|
||||
</VBtn>
|
||||
<p v-if="feedback" role="status" class="text-sm" :class="feedback.ok ? 'text-success' : 'text-danger'">
|
||||
{{ feedback.message }}
|
||||
</p>
|
||||
</div>
|
||||
</VCard>
|
||||
</VPage>
|
||||
</template>
|
||||
Reference in New Issue
Block a user