160 lines
6.2 KiB
Vue
160 lines
6.2 KiB
Vue
<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 LayoutTitleMedia from './layouts/LayoutTitleMedia.vue';
|
||
import LayoutMosaic from './layouts/LayoutMosaic.vue';
|
||
import LayoutMarquee from './layouts/LayoutMarquee.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,
|
||
title_media: LayoutTitleMedia,
|
||
mosaic: LayoutMosaic,
|
||
marquee: LayoutMarquee,
|
||
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)
|
||
// 内容区最大宽度(config.container_width):'1200'(默认)/ '1400' / '1600' / 'full',
|
||
// 通过 --sec-cw 下发给 section-container shortcut(见 uno.config.ts)
|
||
// 区块圆角(config.border_radius):单位 px,未配置或 0 为直角(默认);
|
||
// 外层 section 已有 overflow-hidden,背景图/视频自动被圆角裁剪
|
||
const borderRadius = computed(() => {
|
||
const r = Number(props.section.config?.border_radius);
|
||
return r > 0 ? `${r}px` : '0px';
|
||
});
|
||
|
||
// 区块垂直内边距(config.padding_y):单位 px,未设置时不下发变量(各布局用 var 回退自身默认值),0 = 完全贴边
|
||
const paddingY = computed(() => {
|
||
const v = props.section.config?.padding_y;
|
||
return typeof v === 'number' && v >= 0 ? `${v}px` : '';
|
||
});
|
||
|
||
// 区块上下外边距(config.margin_y):单位 px,区块与相邻区块的间隙(露出页面背景);
|
||
// 未设置或 0 = 紧贴相邻区块(默认,全宽无缝拼接设计)
|
||
const marginY = computed(() => {
|
||
const v = props.section.config?.margin_y;
|
||
return typeof v === 'number' && v > 0 ? `${v}px` : '';
|
||
});
|
||
|
||
// 标题字号(config.title_font_size):桌面端 px,未设置时不下发变量(main.css 的 section h2 规则兜底 36px);
|
||
// 移动端按桌面值 62.5% 等比缩小(--sec-title-fs-m)
|
||
const titleFontSize = computed(() => {
|
||
const v = props.section.config?.title_font_size;
|
||
if (typeof v !== 'number' || v <= 0) return null;
|
||
return { desktop: `${v}px`, mobile: `${Math.round(v * 0.625)}px` };
|
||
});
|
||
|
||
const sectionStyleVars = computed<CSSProperties>(() => {
|
||
const w = Number(props.section.config?.width_percent);
|
||
const cw = props.section.config?.container_width;
|
||
const containerWidth = cw === 'full' ? '100%' : Number(cw) > 0 ? `${cw}px` : '1200px';
|
||
return {
|
||
'--sec-w': w > 0 && w < 100 ? `${w}%` : '100%',
|
||
'--sec-cw': containerWidth,
|
||
borderRadius: borderRadius.value,
|
||
...(paddingY.value ? { '--sec-py': paddingY.value } : {}),
|
||
...(marginY.value ? { marginTop: marginY.value, marginBottom: marginY.value } : {}),
|
||
...(titleFontSize.value ? { '--sec-title-fs': titleFontSize.value.desktop, '--sec-title-fs-m': titleFontSize.value.mobile } : {})
|
||
} 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, sectionStyleVars]"
|
||
>
|
||
<!-- 背景图:显示模式由 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>
|