Files
admin/www/app/pages/about.vue
T

128 lines
5.4 KiB
Vue
Raw Normal View History

2026-08-03 19:09:03 +08:00
<script setup lang="ts">
import type { AboutConfig, AboutFeature } from '~/types/site';
import { parseJson } from '~/utils/parseJson';
const { t, locale } = useI18n();
const localePath = useLocalePath();
const { getPageConfig } = useSiteApi();
// SEO:关于我们
await usePageSeo('about', undefined, `${t('app.name')} - ${t('nav.about')}`);
const config = ref<AboutConfig | null>(null);
const loading = ref(true);
onMounted(async () => {
try {
config.value = await getPageConfig<AboutConfig>('about');
} catch {
config.value = null;
} finally {
loading.value = false;
}
});
// ===== 按语言取值 =====
const heroTitle = computed(() => (locale.value === 'en' ? config.value?.hero_title_en : config.value?.hero_title_zh) || (locale.value === 'en' ? 'About Us' : '关于我们'));
const heroSubtitle = computed(() => (locale.value === 'en' ? config.value?.hero_subtitle_en : config.value?.hero_subtitle_zh) || '');
const s1Title = computed(() => (locale.value === 'en' ? config.value?.s1_title_en : config.value?.s1_title_zh) || '');
const s1Content = computed(() => (locale.value === 'en' ? config.value?.s1_content_en : config.value?.s1_content_zh) || '');
const s1Cta = computed(() => (locale.value === 'en' ? config.value?.s1_cta_en : config.value?.s1_cta_zh) || '');
const s2Title = computed(() => (locale.value === 'en' ? config.value?.s2_title_en : config.value?.s2_title_zh) || '');
const s3Title = computed(() => (locale.value === 'en' ? config.value?.s3_title_en : config.value?.s3_title_zh) || '');
const s3Cta = computed(() => (locale.value === 'en' ? config.value?.s3_cta_en : config.value?.s3_cta_zh) || '');
// s2_featuresJSON 字符串或数组
const features = computed<AboutFeature[]>(() =>
parseJson<AboutFeature[]>(config.value?.s2_features, [])
);
function featureTitle(f: AboutFeature): string {
return locale.value === 'en' ? f.title_en : f.title_zh;
}
function featureDesc(f: AboutFeature): string {
return locale.value === 'en' ? f.desc_en : f.desc_zh;
}
// 是否需要展示各区块
const hasS1 = computed(() => Boolean(s1Title.value || s1Content.value || config.value?.s1_image_url));
const hasS2 = computed(() => Boolean(s2Title.value && features.value.length));
const hasS3 = computed(() => Boolean(s3Title.value || s3Cta.value));
</script>
<template>
<div>
<PageHero
:title="heroTitle"
:subtitle="heroSubtitle || undefined"
:bg-url="config?.hero_bg_url || undefined"
/>
<div v-if="loading" class="py-80px text-center text-sm text-gray-400">
{{ t('common.loading') }}
</div>
<div v-else>
2026-08-10 18:50:14 +08:00
<!-- S1图文介绍上文下图 -->
2026-08-03 19:09:03 +08:00
<section v-if="hasS1" class="section-container py-64px md:py-96px">
2026-08-10 18:50:14 +08:00
<div class="flex flex-col gap-40px md:gap-48px">
<div class="mx-auto max-w-800px flex flex-col gap-20px">
2026-08-03 19:09:03 +08:00
<h2 v-if="s1Title" class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white">{{ s1Title }}</h2>
<!-- 富文本dashboard 端为 HTML 内容 -->
<div v-if="s1Content" class="text-base md:text-lg text-gray-600 dark:text-gray-300 leading-relaxed" v-html="s1Content" />
<div v-if="s1Cta">
<NuxtLink
:to="localePath(config?.s1_cta_url || '/contact')"
class="inline-block px-28px py-12px rounded-8px bg-primary text-white text-sm font-medium hover:opacity-90 transition-opacity"
>
{{ s1Cta }}
</NuxtLink>
</div>
</div>
2026-08-10 18:50:14 +08:00
<div v-if="config?.s1_image_url">
2026-08-03 19:09:03 +08:00
<img
:src="config.s1_image_url"
:alt="s1Title"
2026-08-10 18:50:14 +08:00
class="w-full rounded-16px shadow-lg object-cover aspect-[16/9]"
2026-08-03 19:09:03 +08:00
/>
</div>
</div>
</section>
<!-- S2核心优势 -->
<section v-if="hasS2" class="bg-gray-50 dark:bg-dark-900 border-y border-gray-100 dark:border-dark-700">
<div class="section-container py-64px md:py-96px">
<h2 class="text-center text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-48px">{{ s2Title }}</h2>
<div class="grid gap-24px sm:grid-cols-2 lg:grid-cols-3">
<div
v-for="(f, idx) in features"
:key="idx"
class="p-24px rounded-16px bg-white dark:bg-dark-800 border border-gray-100 dark:border-dark-700 hover:shadow-lg transition-shadow"
>
<h3 class="font-semibold text-gray-900 dark:text-white mb-8px">{{ featureTitle(f) }}</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">{{ featureDesc(f) }}</p>
</div>
</div>
</div>
</section>
<!-- S3CTA -->
<section v-if="hasS3" class="section-container py-64px md:py-96px text-center">
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-32px">{{ s3Title }}</h2>
<NuxtLink
v-if="s3Cta"
:to="localePath(config?.s3_cta_url || '/contact')"
class="inline-block px-32px py-12px rounded-8px bg-primary text-white text-sm font-medium hover:opacity-90 transition-opacity"
>
{{ s3Cta }}
</NuxtLink>
</section>
<!-- 空配置提示 -->
<div v-if="!hasS1 && !hasS2 && !hasS3" class="section-container py-80px text-center">
<p class="text-gray-400">{{ t('common.empty') }}</p>
</div>
</div>
</div>
</template>