官网开发中 0731
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { ctaPrimaryText, ctaSecondaryText, isDark } = useSectionContent(props.section);
|
||||
const { isExternal, resolveLink } = useLinkResolver();
|
||||
|
||||
const primaryLink = computed(() => resolveLink(props.section.cta_primary_url));
|
||||
const secondaryLink = computed(() => resolveLink(props.section.cta_secondary_url));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="primaryLink || secondaryLink" class="flex flex-wrap items-center gap-16px">
|
||||
<!-- 主 CTA -->
|
||||
<template v-if="primaryLink && ctaPrimaryText">
|
||||
<a
|
||||
v-if="isExternal(section.cta_primary_url)"
|
||||
:href="section.cta_primary_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-28px py-12px rounded-full text-sm font-medium transition-all"
|
||||
:class="isDark ? 'bg-white text-gray-900 hover:bg-gray-200' : 'bg-primary text-white hover:opacity-90'"
|
||||
>
|
||||
{{ ctaPrimaryText }}
|
||||
</a>
|
||||
<NuxtLink
|
||||
v-else
|
||||
:to="primaryLink"
|
||||
class="px-28px py-12px rounded-full text-sm font-medium transition-all"
|
||||
:class="isDark ? 'bg-white text-gray-900 hover:bg-gray-200' : 'bg-primary text-white hover:opacity-90'"
|
||||
>
|
||||
{{ ctaPrimaryText }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<!-- 次 CTA -->
|
||||
<template v-if="secondaryLink && ctaSecondaryText">
|
||||
<a
|
||||
v-if="isExternal(section.cta_secondary_url)"
|
||||
:href="section.cta_secondary_url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-28px py-12px rounded-full text-sm font-medium border transition-all"
|
||||
:class="isDark ? 'border-white/40 text-white hover:bg-white/10' : 'border-gray-300 text-gray-700 hover:border-primary hover:text-primary dark:border-dark-600 dark:text-gray-200'"
|
||||
>
|
||||
{{ ctaSecondaryText }}
|
||||
</a>
|
||||
<NuxtLink
|
||||
v-else
|
||||
:to="secondaryLink"
|
||||
class="px-28px py-12px rounded-full text-sm font-medium border transition-all"
|
||||
:class="isDark ? 'border-white/40 text-white hover:bg-white/10' : 'border-gray-300 text-gray-700 hover:border-primary hover:text-primary dark:border-dark-600 dark:text-gray-200'"
|
||||
>
|
||||
{{ ctaSecondaryText }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock, SectionLayout } from '~/types/site';
|
||||
import LayoutHeroSplit from './layouts/LayoutHeroSplit.vue';
|
||||
import LayoutFullBanner from './layouts/LayoutFullBanner.vue';
|
||||
import LayoutFeatureGrid from './layouts/LayoutFeatureGrid.vue';
|
||||
import LayoutSplitContent from './layouts/LayoutSplitContent.vue';
|
||||
import LayoutGallery from './layouts/LayoutGallery.vue';
|
||||
import LayoutStats from './layouts/LayoutStats.vue';
|
||||
import LayoutVideoShowcase from './layouts/LayoutVideoShowcase.vue';
|
||||
import LayoutScrollNarrative from './layouts/LayoutScrollNarrative.vue';
|
||||
import LayoutQuote from './layouts/LayoutQuote.vue';
|
||||
import LayoutSpecTable from './layouts/LayoutSpecTable.vue';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
// 布局类型 -> 组件映射
|
||||
const layoutComponents: Record<SectionLayout, unknown> = {
|
||||
hero_split: LayoutHeroSplit,
|
||||
full_banner: LayoutFullBanner,
|
||||
feature_grid: LayoutFeatureGrid,
|
||||
split_content: LayoutSplitContent,
|
||||
gallery: LayoutGallery,
|
||||
stats: LayoutStats,
|
||||
video_showcase: LayoutVideoShowcase,
|
||||
scroll_narrative: LayoutScrollNarrative,
|
||||
quote: LayoutQuote,
|
||||
spec_table: LayoutSpecTable
|
||||
};
|
||||
|
||||
const layoutComponent = computed(() => layoutComponents[props.section.layout] ?? null);
|
||||
|
||||
const isDark = computed(() => props.section.theme === 'dark');
|
||||
|
||||
// 遮罩透明度(decimal 字符串 -> 数值)
|
||||
const overlayOpacity = computed(() => Number(props.section.overlay_opacity) || 0);
|
||||
const showOverlay = computed(() => !!props.section.overlay_enabled && overlayOpacity.value > 0);
|
||||
|
||||
// 背景样式
|
||||
const bgStyle = computed(() => {
|
||||
const { bg_type, bg_value } = props.section;
|
||||
if (bg_type === 'color' && bg_value) {
|
||||
return { backgroundColor: bg_value };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const hasBgImage = computed(() => props.section.bg_type === 'image' && !!props.section.bg_value);
|
||||
const hasBgVideo = computed(() => props.section.bg_type === 'video' && !!props.section.bg_value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="relative overflow-hidden"
|
||||
:class="isDark ? 'bg-gray-950 text-white' : 'bg-white text-gray-900 dark:bg-dark-900 dark:text-white'"
|
||||
:style="bgStyle"
|
||||
>
|
||||
<!-- 背景图 -->
|
||||
<div
|
||||
v-if="hasBgImage"
|
||||
class="absolute inset-0 bg-cover bg-center"
|
||||
:style="{ backgroundImage: `url(${section.bg_value})` }"
|
||||
/>
|
||||
|
||||
<!-- 背景视频 -->
|
||||
<video
|
||||
v-if="hasBgVideo"
|
||||
:src="section.bg_value"
|
||||
class="absolute inset-0 w-full h-full object-cover"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
v-if="showOverlay"
|
||||
class="absolute inset-0 bg-black"
|
||||
:style="{ opacity: overlayOpacity }"
|
||||
/>
|
||||
|
||||
<!-- 布局内容 -->
|
||||
<div class="relative z-10">
|
||||
<component :is="layoutComponent" v-if="layoutComponent" :section="section" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark } = useSectionContent(props.section);
|
||||
|
||||
const features = computed(() => config.value.features ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 特性网格 -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-24px">
|
||||
<div
|
||||
v-for="(feature, idx) in features"
|
||||
:key="idx"
|
||||
class="p-32px rounded-16px transition-all hover:-translate-y-4px"
|
||||
:class="isDark ? 'bg-white/5 border border-white/10' : 'bg-white border border-gray-100 shadow-sm hover:shadow-md dark:bg-dark-800 dark:border-dark-700'"
|
||||
>
|
||||
<span v-if="feature.icon" class="text-3xl">{{ feature.icon }}</span>
|
||||
<h3 class="mt-16px text-lg font-semibold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ isEn ? feature.title_en : feature.title_zh }}
|
||||
</h3>
|
||||
<p class="mt-8px text-sm leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ isEn ? feature.desc_en : feature.desc_zh }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, content } = useSectionContent(props.section);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-120px flex-col-center text-center max-w-800px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="section.theme === 'dark' ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-16px text-3xl md:text-5xl font-bold leading-tight" :class="section.theme === 'dark' ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-20px text-lg md:text-xl" :class="section.theme === 'dark' ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="mt-16px text-15px leading-relaxed" :class="section.theme === 'dark' ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-32px justify-center" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark } = useSectionContent(props.section);
|
||||
|
||||
const images = computed(() => config.value.images ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 图片网格 -->
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-16px">
|
||||
<figure v-for="(img, idx) in images" :key="idx" class="group relative overflow-hidden rounded-12px">
|
||||
<img :src="img.url" :alt="isEn ? img.caption_en : img.caption_zh" class="w-full aspect-4/3 object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
<figcaption
|
||||
v-if="(isEn ? img.caption_en : img.caption_zh)"
|
||||
class="absolute bottom-0 left-0 right-0 px-16px py-12px text-sm text-white bg-gradient-to-t from-black/60 to-transparent"
|
||||
>
|
||||
{{ isEn ? img.caption_en : img.caption_zh }}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, content, mediaUrl, isDark } = useSectionContent(props.section);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container aspect-[2/3] md:aspect-auto md:min-h-[min(calc(92vh-64px),54vw)] grid md:grid-cols-2 gap-48px items-center">
|
||||
<!-- 左侧文案 -->
|
||||
<div class="flex flex-col gap-20px self-center md:min-h-[680px]">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="text-3xl md:text-5xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-8px" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧图片 -->
|
||||
<div v-if="mediaUrl" class="flex-center">
|
||||
<img :src="mediaUrl" :alt="title" class="w-full max-w-560px rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
// 引言评价:正文 content 作为引用语,subtitle 作为作者/出处
|
||||
const { content, subtitle, isDark } = useSectionContent(props.section);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px flex-col-center text-center max-w-800px">
|
||||
<span class="text-6xl leading-none font-serif" :class="isDark ? 'text-primary-300' : 'text-primary'">“</span>
|
||||
<blockquote
|
||||
class="mt-8px text-2xl md:text-3xl font-medium leading-relaxed"
|
||||
:class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'"
|
||||
v-html="content"
|
||||
/>
|
||||
<p v-if="subtitle" class="mt-24px text-sm tracking-wide" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
—— {{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark } = useSectionContent(props.section);
|
||||
|
||||
const steps = computed(() => config.value.steps ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 步骤叙事 -->
|
||||
<div class="flex flex-col gap-64px max-w-960px mx-auto">
|
||||
<div
|
||||
v-for="(step, idx) in steps"
|
||||
:key="idx"
|
||||
class="grid md:grid-cols-2 gap-32px items-center"
|
||||
>
|
||||
<!-- 文案 -->
|
||||
<div class="flex flex-col gap-12px" :class="idx % 2 === 1 ? 'md:order-2' : ''">
|
||||
<span class="text-4xl font-bold opacity-20" :class="isDark ? 'text-white' : 'text-gray-900'">
|
||||
{{ String(idx + 1).padStart(2, '0') }}
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ isEn ? step.title_en : step.title_zh }}
|
||||
</h3>
|
||||
<p class="text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ isEn ? step.desc_en : step.desc_zh }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 配图 -->
|
||||
<div v-if="step.image_url" :class="idx % 2 === 1 ? 'md:order-1' : ''">
|
||||
<img :src="step.image_url" :alt="isEn ? step.title_en : step.title_zh" class="w-full rounded-16px object-cover shadow-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock, SpecGroup } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, isEn, isDark } = useSectionContent(props.section);
|
||||
|
||||
const specGroups = computed<SpecGroup[]>(() => props.section.spec_groups ?? []);
|
||||
|
||||
// 每个分组的展开状态(默认收起,仅显示前 default_visible 项)
|
||||
const expandedMap = ref<Record<number, boolean>>({});
|
||||
|
||||
function isExpanded(group: SpecGroup): boolean {
|
||||
return !!expandedMap.value[group.id];
|
||||
}
|
||||
|
||||
function toggle(group: SpecGroup) {
|
||||
expandedMap.value[group.id] = !expandedMap.value[group.id];
|
||||
}
|
||||
|
||||
// 分组内可见的参数项:展开时全部,收起时前 default_visible 项
|
||||
function visibleItems(group: SpecGroup) {
|
||||
const items = group.items ?? [];
|
||||
if (isExpanded(group)) return items;
|
||||
return items.slice(0, group.default_visible || 5);
|
||||
}
|
||||
|
||||
// 是否需要显示展开按钮(参数项 > 默认显示数量)
|
||||
function needToggle(group: SpecGroup): boolean {
|
||||
return (group.items?.length ?? 0) > (group.default_visible || 5);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 参数分组卡片 -->
|
||||
<div class="flex flex-col gap-24px max-w-960px mx-auto">
|
||||
<div
|
||||
v-for="group in specGroups"
|
||||
:key="group.id"
|
||||
class="rounded-16px overflow-hidden"
|
||||
:class="isDark ? 'bg-white/5 border border-white/10' : 'bg-white border border-gray-100 shadow-sm dark:bg-dark-800 dark:border-dark-700'"
|
||||
>
|
||||
<!-- 分组标题 -->
|
||||
<div class="px-32px py-20px border-b" :class="isDark ? 'border-white/10' : 'border-gray-100 dark:border-dark-700'">
|
||||
<h3 class="text-lg font-semibold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ isEn ? group.title_en : group.title_zh }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- 参数项列表 -->
|
||||
<div class="px-32px">
|
||||
<div
|
||||
v-for="item in visibleItems(group)"
|
||||
:key="item.id"
|
||||
class="flex items-start justify-between gap-24px py-14px border-b last:border-b-0"
|
||||
:class="isDark ? 'border-white/5' : 'border-gray-50 dark:border-dark-700/50'"
|
||||
>
|
||||
<span class="text-sm shrink-0" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ isEn ? item.name_en : item.name_zh }}
|
||||
</span>
|
||||
<span class="text-sm text-right font-medium" :class="isDark ? 'text-gray-200' : 'text-gray-800 dark:text-gray-200'">
|
||||
{{ isEn ? item.value_en : item.value_zh }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 展开/收起按钮 -->
|
||||
<div v-if="needToggle(group)" class="px-32px py-16px">
|
||||
<button
|
||||
class="text-sm font-medium transition-colors"
|
||||
:class="isDark ? 'text-primary-300 hover:text-primary-200' : 'text-primary hover:opacity-80'"
|
||||
@click="toggle(group)"
|
||||
>
|
||||
{{ isExpanded(group) ? '收起参数 ∧' : `查看全部参数(${group.items?.length ?? 0} 项)∨` }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, content, mediaUrl, isDark } = useSectionContent(props.section);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px grid md:grid-cols-2 gap-48px items-center">
|
||||
<!-- 图片 -->
|
||||
<div v-if="mediaUrl" class="flex-center order-2 md:order-1">
|
||||
<img :src="mediaUrl" :alt="title" class="w-full max-w-520px rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
|
||||
<!-- 文案 -->
|
||||
<div class="flex flex-col gap-20px order-1 md:order-2">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="text-3xl md:text-4xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-lg" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-8px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark } = useSectionContent(props.section);
|
||||
|
||||
const stats = computed(() => config.value.stats ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-32px">
|
||||
<div v-for="(stat, idx) in stats" :key="idx" class="flex-col-center text-center">
|
||||
<p class="text-4xl md:text-5xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ stat.value }}<span class="text-2xl md:text-3xl ml-4px opacity-70">{{ stat.unit }}</span>
|
||||
</p>
|
||||
<p class="mt-12px text-sm" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ isEn ? stat.label_en : stat.label_zh }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, isDark } = useSectionContent(props.section);
|
||||
|
||||
// 判断是否为可嵌入的 iframe 视频(YouTube / Bilibili)
|
||||
const isEmbed = computed(() => /youtube\.com|youtu\.be|bilibili\.com|player\.bilibili\.com/.test(props.section.video_url));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-48px">
|
||||
<p v-if="overline" class="text-13px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 视频 -->
|
||||
<div class="max-w-960px mx-auto rounded-16px overflow-hidden shadow-xl bg-black aspect-16/9">
|
||||
<iframe
|
||||
v-if="isEmbed"
|
||||
:src="section.video_url"
|
||||
class="w-full h-full"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
scrolling="no"
|
||||
/>
|
||||
<video
|
||||
v-else-if="section.video_url"
|
||||
:src="section.video_url"
|
||||
class="w-full h-full object-cover"
|
||||
controls
|
||||
playsinline
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user