Files
admin/www/app/components/section/layouts/LayoutMarquee.vue
T
2026-08-11 18:42:11 +08:00

96 lines
3.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>