Files
admin/www/app/components/section/layouts/LayoutMosaic.vue
T
2026-08-05 18:36:01 +08:00

111 lines
4.7 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);
type MosaicPattern = 'big_3' | 'half_2' | 'big_4' | 'big_2';
// 拼贴版式(config.mosaic_pattern):big_3 一大三小(默认)/ half_2 左右等分 / big_4 一大四小 / big_2 一大二小上下
const pattern = computed<MosaicPattern>(() => {
const p = config.value.mosaic_pattern;
return p === 'half_2' || p === 'big_4' || p === 'big_2' ? p : 'big_3';
});
// 各版式槽位数量
const slotCount: Record<MosaicPattern, number> = { big_3: 4, half_2: 2, big_4: 5, big_2: 3 };
// 槽位素材(config.mosaic_items):media_zh/_en 以语言后缀结尾,不命中数据层 resolveAssetUrls,需手动解析相对路径
const items = computed(() => {
const raw = (config.value.mosaic_items ?? []) as Array<{ type?: string; media_zh?: string; media_en?: string }>;
return Array.from({ length: slotCount[pattern.value] }, (_, i) => {
const it = raw[i] ?? {};
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) };
});
});
// 网格高度(config.min_height,与主视觉分屏共用字段):仅桌面端生效;
// 未设置 = 默认 16/9 宽高比;>0 = 固定高度 px(格子媒体 object-cover 裁剪填充),移动端保持单列 4/3 堆叠
const gridHeight = computed(() => {
const v = (props.section.config as Record<string, unknown> | null)?.min_height;
return typeof v === 'number' && v > 0 ? `${v}px` : '';
});
// 容器网格 class(完整字面量供 UnoCSS 提取;容器定高后行轨道均分,格子内媒体 object-cover 填充)
const gridClass = computed(() => {
const base = (
{
big_3: 'md:grid-cols-4 md:grid-rows-2',
half_2: 'md:grid-cols-2',
big_4: 'md:grid-cols-4 md:grid-rows-2',
big_2: 'md:grid-cols-2 md:grid-rows-2'
} as Record<MosaicPattern, string>
)[pattern.value];
// 自定义高度时改用固定高(md:h-[var(--mosaic-h)]),否则保持默认宽高比
const sizing = gridHeight.value
? 'md:h-[var(--mosaic-h)]'
: (
{
big_3: 'md:aspect-16/9',
half_2: '',
big_4: 'md:aspect-16/9',
big_2: 'md:aspect-16/9'
} as Record<MosaicPattern, string>
)[pattern.value];
return `${base} ${sizing}`;
});
// 格子跨行跨列 class(按槽位下标;移动端统一单列 4/3 堆叠)
function cellClass(idx: number) {
switch (pattern.value) {
case 'big_3':
if (idx === 0) return 'aspect-4/3 md:aspect-auto md:col-span-2 md:row-span-2';
if (idx === 3) return 'aspect-4/3 md:aspect-auto md:col-span-2';
return 'aspect-4/3 md:aspect-auto';
case 'half_2':
return 'aspect-4/3';
case 'big_4':
if (idx === 0) return 'aspect-4/3 md:aspect-auto md:col-span-2 md:row-span-2';
return 'aspect-4/3 md:aspect-auto';
case 'big_2':
if (idx === 0) return 'aspect-4/3 md:aspect-auto md:row-span-2';
return 'aspect-4/3 md:aspect-auto';
default:
return 'aspect-4/3';
}
}
</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>
<!-- 拼贴网格自定义高度时通过 --mosaic-h 下发仅桌面端 -->
<div class="grid gap-16px" :class="gridClass" :style="gridHeight ? { '--mosaic-h': gridHeight } : {}">
<div
v-for="(item, idx) in items"
:key="idx"
class="overflow-hidden rounded-16px bg-gray-100 dark:bg-gray-800"
:class="cellClass(idx)"
>
<video v-if="item.type === 'video' && item.url" :src="item.url" class="w-full h-full object-cover" autoplay muted loop playsinline preload="metadata" />
<img v-else-if="item.url" :src="item.url" :alt="title" class="w-full h-full object-cover" loading="lazy" />
</div>
</div>
</div>
</template>