feat(admin,shop-admin): content/brand management + merchant shop profile (add-content-admin-ui)
This commit is contained in:
@@ -26,6 +26,7 @@ watchEffect(() => {
|
||||
<nav class="grid gap-1" :aria-label="$t('common.appName')">
|
||||
<NuxtLink to="/" exact-active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.dashboard") }}</NuxtLink>
|
||||
<NuxtLink to="/products" active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.products") }}</NuxtLink>
|
||||
<NuxtLink to="/shop-profile" active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.shopProfile") }}</NuxtLink>
|
||||
<NuxtLink to="/orders" active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.orders") }}</NuxtLink>
|
||||
<NuxtLink to="/shipments" active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.shipments") }}</NuxtLink>
|
||||
<NuxtLink to="/invoices" active-class="bg-primary-soft text-primary" class="rounded-md px-3 py-2 text-sm font-medium text-muted hover:bg-bg">{{ $t("nav.invoices") }}</NuxtLink>
|
||||
|
||||
@@ -8,8 +8,21 @@ export const enExtra = {
|
||||
coupons: "Coupons",
|
||||
flashSales: "Flash sales",
|
||||
groupBuying: "Group buying",
|
||||
shopProfile: "Shop profile",
|
||||
},
|
||||
shop: {
|
||||
profileSaved: "Shop profile saved.",
|
||||
bilingualRequired: "Both English and Chinese text are required.",
|
||||
profileLogo: "Logo URL",
|
||||
profileBanner: "Banner URL",
|
||||
profileCompany: "Company",
|
||||
profileRegion: "Region",
|
||||
profileAddressEn: "Address (English)",
|
||||
profileAddressZh: "Address (中文)",
|
||||
profileNoticeEn: "Notice (English)",
|
||||
profileNoticeZh: "Notice (中文)",
|
||||
profileAfterSaleEn: "After-sale policy (English)",
|
||||
profileAfterSaleZh: "After-sale policy (中文)",
|
||||
dashboardTitle: "Merchant dashboard",
|
||||
profile: "Shop profile",
|
||||
shopStatus: "Shop status",
|
||||
@@ -156,8 +169,21 @@ export const zhExtra = {
|
||||
coupons: "优惠券",
|
||||
flashSales: "秒杀",
|
||||
groupBuying: "拼团",
|
||||
shopProfile: "店铺资料",
|
||||
},
|
||||
shop: {
|
||||
profileSaved: "店铺资料已保存。",
|
||||
bilingualRequired: "中英文内容均为必填项。",
|
||||
profileLogo: "Logo 链接",
|
||||
profileBanner: "横幅链接",
|
||||
profileCompany: "公司",
|
||||
profileRegion: "地区",
|
||||
profileAddressEn: "地址(英文)",
|
||||
profileAddressZh: "地址(中文)",
|
||||
profileNoticeEn: "公告(英文)",
|
||||
profileNoticeZh: "公告(中文)",
|
||||
profileAfterSaleEn: "售后政策(英文)",
|
||||
profileAfterSaleZh: "售后政策(中文)",
|
||||
dashboardTitle: "商家仪表盘",
|
||||
profile: "店铺信息",
|
||||
shopStatus: "店铺状态",
|
||||
|
||||
@@ -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