官网开发 0811
This commit is contained in:
@@ -8,11 +8,9 @@ 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 LayoutTitleMedia from './layouts/LayoutTitleMedia.vue';
|
||||
import LayoutMosaic from './layouts/LayoutMosaic.vue';
|
||||
import LayoutScrollNarrative from './layouts/LayoutScrollNarrative.vue';
|
||||
import LayoutQuote from './layouts/LayoutQuote.vue';
|
||||
import LayoutMarquee from './layouts/LayoutMarquee.vue';
|
||||
import LayoutSpecTable from './layouts/LayoutSpecTable.vue';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
@@ -25,11 +23,9 @@ const layoutComponents: Record<SectionLayout, unknown> = {
|
||||
split_content: LayoutSplitContent,
|
||||
gallery: LayoutGallery,
|
||||
stats: LayoutStats,
|
||||
video_showcase: LayoutVideoShowcase,
|
||||
title_media: LayoutTitleMedia,
|
||||
mosaic: LayoutMosaic,
|
||||
scroll_narrative: LayoutScrollNarrative,
|
||||
quote: LayoutQuote,
|
||||
marquee: LayoutMarquee,
|
||||
spec_table: LayoutSpecTable
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||
|
||||
// 轮播素材(config.marquee_items):media_zh/_en 以语言后缀结尾,不命中数据层 resolveAssetUrls,需手动解析相对路径;最多 5 个
|
||||
const items = computed(() => {
|
||||
const raw = (config.value.marquee_items ?? []) as Array<{ type?: string; media_zh?: string; media_en?: string; link?: string }>;
|
||||
return raw
|
||||
.slice(0, 5)
|
||||
.map(it => {
|
||||
const src = (isEn.value ? it.media_en || it.media_zh : it.media_zh || it.media_en) || '';
|
||||
return { type: it.type === 'video' ? 'video' : 'image', url: resolveAssetUrl(src), link: (it.link || '').trim() };
|
||||
})
|
||||
.filter(it => !!it.url);
|
||||
});
|
||||
|
||||
// 点击素材:若配置了跳转链接则新窗口打开
|
||||
function handleItemClick(item: { link?: string }) {
|
||||
if (item.link) window.open(item.link, '_blank', 'noopener');
|
||||
}
|
||||
|
||||
// 切换间隔(config.marquee_interval,秒):默认 3s,限制 1-60
|
||||
const interval = computed(() => {
|
||||
const v = Number(config.value.marquee_interval);
|
||||
return Number.isFinite(v) && v >= 1 ? Math.min(v, 60) : 3;
|
||||
});
|
||||
|
||||
const current = ref(0);
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
function stopTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
// 素材/间隔变化时重启计时器,并收敛下标防越界
|
||||
function startTimer() {
|
||||
stopTimer();
|
||||
if (current.value >= items.value.length) current.value = 0;
|
||||
if (items.value.length < 2) return;
|
||||
timer = setInterval(() => {
|
||||
current.value = (current.value + 1) % items.value.length;
|
||||
}, interval.value * 1000);
|
||||
}
|
||||
watch([items, interval], startTimer, { immediate: true });
|
||||
onBeforeUnmount(stopTimer);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div v-if="overline || title || subtitle" class="text-center max-w-640px mx-auto mb-56px">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 轮播容器:淡入淡出切换 -->
|
||||
<div v-if="items.length" class="relative overflow-hidden rounded-16px bg-gray-100 dark:bg-gray-800 aspect-16/9">
|
||||
<div
|
||||
v-for="(item, idx) in items"
|
||||
:key="idx"
|
||||
class="absolute inset-0 transition-opacity duration-500"
|
||||
:class="[idx === current ? 'opacity-100' : 'opacity-0 pointer-events-none', item.link ? 'cursor-pointer' : '']"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<video v-if="item.type === 'video'" :src="item.url" class="w-full h-full object-cover" autoplay muted loop playsinline preload="metadata" />
|
||||
<img v-else :src="item.url" :alt="title" class="w-full h-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
|
||||
<!-- 指示点(可点击切换) -->
|
||||
<div v-if="items.length > 1" class="absolute bottom-16px left-0 right-0 flex justify-center gap-8px">
|
||||
<button
|
||||
v-for="(item, idx) in items"
|
||||
:key="idx"
|
||||
type="button"
|
||||
class="h-8px rounded-full transition-all"
|
||||
:class="idx === current ? 'w-24px bg-white' : 'w-8px bg-white/50'"
|
||||
@click="current = idx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
// 引言评价:正文 content 作为引用语,subtitle 作为作者/出处
|
||||
const { content, subtitle, isDark, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-[var(--sec-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="rich-text mt-8px text-2xl md:text-3xl font-medium leading-relaxed"
|
||||
:class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'"
|
||||
:style="titleStyle"
|
||||
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'" :style="bodyStyle">
|
||||
—— {{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,53 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, config, isEn, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||
|
||||
const steps = computed(() => config.value.steps ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
||||
{{ 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'" :style="titleStyle">
|
||||
{{ 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'" :style="bodyStyle">
|
||||
{{ 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>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, isDark, overlineStyle, titleStyle, bodyStyle } = 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-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-48px">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="isDark ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-12px font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-16px text-base" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle">
|
||||
{{ 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