Files
admin/www/app/components/section/SectionRenderer.vue
T

121 lines
4.3 KiB
Vue
Raw Normal View History

2026-07-31 19:02:00 +08:00
<script setup lang="ts">
2026-08-04 19:18:35 +08:00
import type { CSSProperties } from 'vue';
2026-07-31 19:02:00 +08:00
import type { SectionBlock, SectionLayout } from '~/types/site';
2026-08-04 19:18:35 +08:00
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
2026-07-31 19:02:00 +08:00
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);
2026-08-04 19:18:35 +08:00
// 背景图/视频资源地址(支持 /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]
);
2026-07-31 19:02:00 +08:00
</script>
<template>
<section
2026-08-04 19:18:35 +08:00
class="relative overflow-hidden md:w-[var(--sec-w)] md:mx-auto"
2026-07-31 19:02:00 +08:00
:class="isDark ? 'bg-gray-950 text-white' : 'bg-white text-gray-900 dark:bg-dark-900 dark:text-white'"
2026-08-04 19:18:35 +08:00
:style="[bgStyle, sectionWidthStyle]"
2026-07-31 19:02:00 +08:00
>
2026-08-04 19:18:35 +08:00
<!-- 背景图显示模式由 config.bg_fit 控制默认铺满 -->
2026-07-31 19:02:00 +08:00
<div
v-if="hasBgImage"
2026-08-04 19:18:35 +08:00
:class="bgImageClass"
:style="{ backgroundImage: `url(${bgMediaUrl})` }"
2026-07-31 19:02:00 +08:00
/>
2026-08-04 19:18:35 +08:00
<!-- 背景视频显示模式由 config.bg_fit 控制默认铺满 -->
2026-07-31 19:02:00 +08:00
<video
v-if="hasBgVideo"
2026-08-04 19:18:35 +08:00
:src="bgMediaUrl"
:class="bgVideoClass"
2026-07-31 19:02:00 +08:00
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>