Files
admin/www/app/components/section/SectionRenderer.vue
T
2026-08-04 19:18:35 +08:00

121 lines
4.3 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 { CSSProperties } from 'vue';
import type { SectionBlock, SectionLayout } from '~/types/site';
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
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);
// 背景图/视频资源地址(支持 /uploads/... 相对路径)
const bgMediaUrl = computed(() => resolveAssetUrl(props.section.bg_value));
// 区块宽度百分比(config.width_percent):占视口宽度比例并居中,
// 未配置或 100 时全宽;md 以下始终全宽(见模板 class)
const sectionWidthStyle = computed<CSSProperties>(() => {
const w = Number(props.section.config?.width_percent);
return { '--sec-w': w > 0 && w < 100 ? `${w}%` : '100%' } as CSSProperties;
});
// 背景图/视频显示模式(config.bg_fit):cover 铺满(默认)/ contain 完整显示 / auto 原尺寸
const bgFit = computed<'cover' | 'contain' | 'auto'>(() => {
const f = props.section.config?.bg_fit;
return f === 'contain' || f === 'auto' ? f : 'cover';
});
const bgImageClass = computed(() =>
({
cover: 'absolute inset-0 bg-cover bg-center',
contain: 'absolute inset-0 bg-contain bg-center bg-no-repeat',
auto: 'absolute inset-0 bg-auto bg-center bg-no-repeat'
})[bgFit.value]
);
const bgVideoClass = computed(() =>
({
cover: 'absolute inset-0 w-full h-full object-cover',
contain: 'absolute inset-0 w-full h-full object-contain',
auto: 'absolute inset-0 w-full h-full object-none'
})[bgFit.value]
);
</script>
<template>
<section
class="relative overflow-hidden md:w-[var(--sec-w)] md:mx-auto"
:class="isDark ? 'bg-gray-950 text-white' : 'bg-white text-gray-900 dark:bg-dark-900 dark:text-white'"
:style="[bgStyle, sectionWidthStyle]"
>
<!-- 背景图显示模式由 config.bg_fit 控制默认铺满 -->
<div
v-if="hasBgImage"
:class="bgImageClass"
:style="{ backgroundImage: `url(${bgMediaUrl})` }"
/>
<!-- 背景视频显示模式由 config.bg_fit 控制默认铺满 -->
<video
v-if="hasBgVideo"
:src="bgMediaUrl"
:class="bgVideoClass"
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>