feat(admin,shop-admin): content/brand management + merchant shop profile (add-content-admin-ui)

This commit is contained in:
Chengdong Zhang
2026-09-23 14:27:00 +08:00
parent 9a749e2551
commit 93e5a05d48
22 changed files with 930 additions and 22 deletions
+146
View File
@@ -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>