官网开发中 0803
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock, Product, ProductSeries } from '~/types/site';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const localePath = useLocalePath();
|
||||
const route = useRoute();
|
||||
const siteStore = useSiteStore();
|
||||
const { getSections, getProducts } = useSiteApi();
|
||||
|
||||
const slug = String(route.params.slug || '');
|
||||
|
||||
// ===== 查找产品(优先用 store 数据,直接访问时主动加载) =====
|
||||
const product = ref<Product | null>(null);
|
||||
const series = ref<ProductSeries | null>(null);
|
||||
const sections = ref<SectionBlock[]>([]);
|
||||
const loading = ref(true);
|
||||
const notFound = ref(false);
|
||||
|
||||
async function loadProduct() {
|
||||
const { productSeries } = storeToRefs(siteStore);
|
||||
// store 未初始化或未命中时,主动拉取一次
|
||||
if (!siteStore.loaded && !siteStore.loading) {
|
||||
await siteStore.init();
|
||||
}
|
||||
for (const s of productSeries.value || []) {
|
||||
const p = (s.products || []).find((item) => item.slug === slug);
|
||||
if (p) {
|
||||
product.value = p;
|
||||
series.value = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 兜底:直接调接口
|
||||
if (!product.value) {
|
||||
try {
|
||||
const data = await getProducts();
|
||||
for (const s of data?.series || []) {
|
||||
const p = (s.products || []).find((item) => item.slug === slug);
|
||||
if (p) {
|
||||
product.value = p;
|
||||
series.value = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadProduct();
|
||||
if (!product.value) {
|
||||
notFound.value = true;
|
||||
return;
|
||||
}
|
||||
// SEO:产品详情页
|
||||
await usePageSeo('product_detail', product.value.id, product.value.name_zh);
|
||||
// 产品详情区块(page_type=product_detail + product_id)
|
||||
const res = await getSections('product_detail', product.value.id);
|
||||
sections.value = res?.items ?? [];
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 同系列其他产品 =====
|
||||
const siblings = computed(() =>
|
||||
(series.value?.products || []).filter((p) => p.slug !== slug)
|
||||
);
|
||||
|
||||
function productName(p: Product): string {
|
||||
return locale.value === 'en' ? p.name_en : p.name_zh;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="min-h-60vh flex-col-center text-sm text-gray-400">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- 产品不存在 -->
|
||||
<div v-else-if="notFound || !product" class="min-h-60vh flex-col-center px-16px text-center">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">404</h1>
|
||||
<p class="mt-12px text-gray-500 dark:text-gray-400">{{ t('common.notFoundDesc') }}</p>
|
||||
<NuxtLink
|
||||
:to="localePath('/products')"
|
||||
class="mt-24px px-24px py-10px rounded-6px bg-primary text-white text-sm hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{{ t('nav.products') }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 产品头部:面包屑 + 名称 + 简介 -->
|
||||
<section class="section-container py-48px md:py-64px flex flex-col gap-12px">
|
||||
<nav class="flex items-center gap-8px text-sm text-gray-400">
|
||||
<NuxtLink :to="localePath('/')" class="hover:text-primary transition-colors">{{ t('nav.home') }}</NuxtLink>
|
||||
<span>/</span>
|
||||
<NuxtLink :to="localePath('/products')" class="hover:text-primary transition-colors">{{ t('nav.products') }}</NuxtLink>
|
||||
<span>/</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ productName(product) }}</span>
|
||||
</nav>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mt-8px">{{ productName(product) }}</h1>
|
||||
<p class="text-base md:text-lg text-gray-500 dark:text-gray-400 max-w-760px leading-relaxed">
|
||||
{{ locale === 'en' ? product.intro_en : product.intro_zh }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- 详情区块:由 CMS 的 Section 组合渲染 -->
|
||||
<SectionRenderer v-for="section in sections" :key="section.id" :section="section" />
|
||||
|
||||
<!-- 空区块提示 -->
|
||||
<div v-if="!sections.length" class="section-container py-64px text-center">
|
||||
<p class="text-gray-400">{{ t('common.empty') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 同系列其他产品 -->
|
||||
<section v-if="siblings.length" class="border-t border-gray-100 dark:border-dark-700 bg-gray-50 dark:bg-dark-900">
|
||||
<div class="section-container py-48px md:py-64px">
|
||||
<h2 class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-24px">
|
||||
{{ t('common.moreInSeries') }}
|
||||
</h2>
|
||||
<div class="grid gap-16px sm:grid-cols-2 lg:grid-cols-3">
|
||||
<NuxtLink
|
||||
v-for="p in siblings"
|
||||
:key="p.id"
|
||||
:to="localePath(`/products/${p.slug}`)"
|
||||
class="group flex items-center gap-16px p-16px rounded-12px border border-gray-100 dark:border-dark-700 bg-white dark:bg-dark-800 hover:shadow-md hover:-translate-y-2px transition-all duration-300"
|
||||
>
|
||||
<div class="w-64px h-48px rounded-8px bg-gray-50 dark:bg-dark-700 overflow-hidden shrink-0">
|
||||
<img v-if="p.cover_url" :src="p.cover_url" :alt="productName(p)" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-4px min-w-0">
|
||||
<span class="font-medium text-gray-900 dark:text-white group-hover:text-primary transition-colors truncate">
|
||||
{{ productName(p) }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400 truncate">{{ locale === 'en' ? p.intro_en : p.intro_zh }}</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user