官网开发 0805
This commit is contained in:
@@ -9,6 +9,8 @@ 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 LayoutTitleMedia from './layouts/LayoutTitleMedia.vue';
|
||||
import LayoutMosaic from './layouts/LayoutMosaic.vue';
|
||||
import LayoutScrollNarrative from './layouts/LayoutScrollNarrative.vue';
|
||||
import LayoutQuote from './layouts/LayoutQuote.vue';
|
||||
import LayoutSpecTable from './layouts/LayoutSpecTable.vue';
|
||||
@@ -24,6 +26,8 @@ const layoutComponents: Record<SectionLayout, unknown> = {
|
||||
gallery: LayoutGallery,
|
||||
stats: LayoutStats,
|
||||
video_showcase: LayoutVideoShowcase,
|
||||
title_media: LayoutTitleMedia,
|
||||
mosaic: LayoutMosaic,
|
||||
scroll_narrative: LayoutScrollNarrative,
|
||||
quote: LayoutQuote,
|
||||
spec_table: LayoutSpecTable
|
||||
@@ -53,9 +57,40 @@ const bgMediaUrl = computed(() => resolveAssetUrl(props.section.bg_value));
|
||||
|
||||
// 区块宽度百分比(config.width_percent):占视口宽度比例并居中,
|
||||
// 未配置或 100 时全宽;md 以下始终全宽(见模板 class)
|
||||
const sectionWidthStyle = computed<CSSProperties>(() => {
|
||||
// 内容区最大宽度(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.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);
|
||||
return { '--sec-w': w > 0 && w < 100 ? `${w}%` : '100%' } as CSSProperties;
|
||||
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 } : {}),
|
||||
...(titleFontSize.value ? { '--sec-title-fs': titleFontSize.value.desktop, '--sec-title-fs-m': titleFontSize.value.mobile } : {})
|
||||
} as CSSProperties;
|
||||
});
|
||||
|
||||
// 背景图/视频显示模式(config.bg_fit):cover 铺满(默认)/ contain 完整显示 / auto 原尺寸
|
||||
@@ -85,7 +120,7 @@ const bgVideoClass = computed(() =>
|
||||
<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]"
|
||||
:style="[bgStyle, sectionStyleVars]"
|
||||
>
|
||||
<!-- 背景图:显示模式由 config.bg_fit 控制(默认铺满) -->
|
||||
<div
|
||||
|
||||
@@ -9,13 +9,13 @@ const features = computed(() => config.value.features ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div 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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
@@ -7,17 +7,17 @@ const { overline, title, subtitle, content, overlineStyle, titleStyle, bodyStyle
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-120px flex-col-center text-center max-w-800px">
|
||||
<div class="section-container py-[var(--sec-py,120px)] flex-col-center text-center max-w-800px">
|
||||
<p v-if="overline" class="text-14px font-semibold tracking-widest uppercase" :class="section.theme === 'dark' ? 'text-primary-300' : 'text-primary'" :style="overlineStyle">
|
||||
{{ overline }}
|
||||
</p>
|
||||
<h2 class="mt-16px text-3xl md:text-5xl font-bold leading-tight" :class="section.theme === 'dark' ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<h2 class="mt-16px font-bold leading-tight" :class="section.theme === 'dark' ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="mt-20px text-lg md:text-xl" :class="section.theme === 'dark' ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="mt-16px text-15px leading-relaxed" :class="section.theme === 'dark' ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<div v-if="content" class="rich-text mt-16px text-15px leading-relaxed" :class="section.theme === 'dark' ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-32px justify-center" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,13 +9,13 @@ const images = computed(() => config.value.images ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div 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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
@@ -28,32 +28,61 @@ const hPad = computed(() => ({
|
||||
center: 'md:px-64px',
|
||||
right: 'md:pl-96px md:pr-128px'
|
||||
} as Record<string, string>)[textPosition.value.split('_')[1] ?? ''] ?? 'md:px-64px');
|
||||
|
||||
// 区块最小高度(config.min_height):未设置 = 沿用首屏高度规约(移动端 2/3 比例 + 桌面视口公式);
|
||||
// 0 = 完全自适应内容(去上下内边距,高度由媒体素材擑开);>0 = 自定义最小高度 px
|
||||
const minHeight = computed<number | null>(() => {
|
||||
const v = (props.section.config as Record<string, unknown> | null)?.min_height;
|
||||
return typeof v === 'number' && v >= 0 ? v : null;
|
||||
});
|
||||
const isAutoFit = computed(() => minHeight.value === 0);
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
const base = 'relative grid gap-48px px-16px md:px-0';
|
||||
// 默认首屏高度:完整字面量供 UnoCSS 提取
|
||||
return minHeight.value === null ? `aspect-[2/3] md:aspect-auto md:min-h-[max(680px,min(calc(92vh-64px),54vw))] ${base}` : base;
|
||||
});
|
||||
const wrapStyle = computed(() => (!isAutoFit.value && minHeight.value ? { minHeight: `${minHeight.value}px` } : {}));
|
||||
|
||||
// 自适应模式下文案区去掉上下内边距,区块高度严格等于内容高度;
|
||||
// 其余模式支持 config.padding_y 覆盖(默认 64px)
|
||||
const textPadClass = computed(() => (isAutoFit.value ? 'py-0' : 'py-[var(--sec-py,64px)]'));
|
||||
|
||||
// 文案块定位:默认桌面端绝对定位覆盖;自适应模式无前景媒体时改为文档流擑高(否则区块会塔缩),有媒体时保持覆盖
|
||||
const textBlockClass = computed(() =>
|
||||
isAutoFit.value && !mediaUrl.value ? 'py-0' : `${textPadClass.value} md:absolute md:inset-0`
|
||||
);
|
||||
|
||||
// 自适应模式下媒体改为文档流(擑开容器高度,桌面端右半对齐);其余模式保持绝对定位铺右半
|
||||
const mediaClass = computed(() =>
|
||||
isAutoFit.value ? 'flex-center md:w-1/2 md:ml-auto' : 'flex-center md:absolute md:top-0 md:right-0 md:bottom-0 md:w-1/2'
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative aspect-[2/3] md:aspect-auto md:min-h-[max(680px,min(calc(92vh-64px),54vw))] grid gap-48px px-16px md:px-0">
|
||||
<!-- 文案块:移动端文档流;桌面端全宽绝对定位 + 九宫格对齐 -->
|
||||
<div :class="wrapClass" :style="wrapStyle">
|
||||
<!-- 文案块:移动端文档流;桌面端全宽绝对定位 + 九宫格对齐(自适应无媒体时文档流擑高) -->
|
||||
<div
|
||||
class="relative z-10 flex flex-col gap-20px py-64px md:py-64px md:absolute md:inset-0"
|
||||
:class="[vAlign, hAlign, hPad]"
|
||||
class="relative z-10 flex flex-col gap-20px"
|
||||
:class="[textBlockClass, vAlign, hAlign, hPad]"
|
||||
>
|
||||
<div class="flex flex-col gap-20px max-w-560px">
|
||||
<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="text-3xl md:text-5xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<h2 class="font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-8px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片:移动端文档流;桌面端占右侧一半 -->
|
||||
<div v-if="mediaUrl" class="flex-center md:absolute md:top-0 md:right-0 md:bottom-0 md:w-1/2">
|
||||
<!-- 图片:移动端文档流;桌面端占右侧一半(自适应模式为文档流擑高) -->
|
||||
<div v-if="mediaUrl" :class="mediaClass">
|
||||
<img :src="mediaUrl" :alt="title" class="w-full max-w-560px rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<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>
|
||||
@@ -8,10 +8,10 @@ const { content, subtitle, isDark, titleStyle, bodyStyle } = useSectionContent(p
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px flex-col-center text-center max-w-800px">
|
||||
<div class="section-container py-[var(--sec-py,96px)] flex-col-center text-center max-w-800px">
|
||||
<span class="text-6xl leading-none font-serif" :class="isDark ? 'text-primary-300' : 'text-primary'">“</span>
|
||||
<blockquote
|
||||
class="mt-8px text-2xl md:text-3xl font-medium leading-relaxed"
|
||||
class="rich-text mt-8px text-2xl md:text-3xl font-medium leading-relaxed"
|
||||
:class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'"
|
||||
:style="titleStyle"
|
||||
v-html="content"
|
||||
|
||||
@@ -9,13 +9,13 @@ const steps = computed(() => config.value.steps ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
@@ -32,13 +32,13 @@ function needToggle(group: SpecGroup): boolean {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div 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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
@@ -12,10 +12,11 @@ const videoUrl = computed(() => resolveAssetUrl(props.section.video_url));
|
||||
// 媒体位置(config.media_position):left 图/视频在左(默认)/ right 在右;移动端始终文案在上
|
||||
const mediaPosition = computed(() => (props.section.config as Record<string, string> | null)?.media_position === 'right' ? 'right' : 'left');
|
||||
|
||||
// 媒体填满所在栏(去掉 520px 上限与栏内居中),高度按原素材比例自适应
|
||||
const mediaWrapClass = computed(() =>
|
||||
mediaPosition.value === 'right'
|
||||
? 'flex-center order-2'
|
||||
: 'flex-center order-2 md:order-1'
|
||||
? 'order-2'
|
||||
: 'order-2 md:order-1'
|
||||
);
|
||||
|
||||
const textWrapClass = computed(() =>
|
||||
@@ -26,19 +27,19 @@ const textWrapClass = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px grid md:grid-cols-2 gap-48px items-center">
|
||||
<div class="section-container py-[var(--sec-py,96px)] grid md:grid-cols-2 gap-48px items-center">
|
||||
<!-- 媒体:视频优先,其次图片 -->
|
||||
<div v-if="videoUrl || mediaUrl" :class="mediaWrapClass">
|
||||
<video
|
||||
v-if="videoUrl"
|
||||
:src="videoUrl"
|
||||
class="w-full max-w-520px rounded-16px object-cover shadow-lg"
|
||||
class="w-full rounded-16px object-cover shadow-lg"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
<img v-else :src="mediaUrl" :alt="title" class="w-full max-w-520px rounded-16px object-cover shadow-lg" />
|
||||
<img v-else :src="mediaUrl" :alt="title" class="w-full rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
|
||||
<!-- 文案 -->
|
||||
@@ -46,13 +47,13 @@ const textWrapClass = computed(() =>
|
||||
<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="text-3xl md:text-4xl font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<h2 class="font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-lg" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
<SectionCta :section="section" class="mt-8px" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,13 +9,13 @@ const stats = computed(() => config.value.stats ?? []);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-64px">
|
||||
<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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { SectionBlock } from '~/types/site';
|
||||
import { resolveAssetUrl } from '~/utils/resolveAssetUrl';
|
||||
|
||||
const props = defineProps<{ section: SectionBlock }>();
|
||||
|
||||
const { overline, title, subtitle, content, mediaUrl, isDark, overlineStyle, titleStyle, bodyStyle } = useSectionContent(props.section);
|
||||
|
||||
// 媒体视频(video_url 有值时优先渲染视频,支持 /uploads/... 相对路径)
|
||||
const videoUrl = computed(() => resolveAssetUrl(props.section.video_url));
|
||||
|
||||
// 文案水平对齐(config.text_position 九宫格仅取水平方向):left 左对齐(默认)/ center 居中 / right 右对齐
|
||||
const hAlign = computed(() => {
|
||||
const col = (props.section.config as Record<string, string> | null)?.text_position?.split('_')[1] ?? 'left';
|
||||
return ({
|
||||
left: { wrap: 'items-start', text: 'text-left' },
|
||||
center: { wrap: 'items-center', text: 'text-center' },
|
||||
right: { wrap: 'items-end', text: 'text-right' }
|
||||
} as Record<string, { wrap: string; text: string }>)[col] ?? { wrap: 'items-start', text: 'text-left' };
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-[var(--sec-py,96px)] flex flex-col gap-48px" :class="hAlign.wrap">
|
||||
<!-- 标题区 -->
|
||||
<div class="flex flex-col gap-16px max-w-800px" :class="hAlign.text">
|
||||
<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="font-bold leading-tight" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<p v-if="subtitle" class="text-lg md:text-xl" :class="isDark ? 'text-gray-300' : 'text-gray-600 dark:text-gray-300'" :style="bodyStyle">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
<div v-if="content" class="rich-text text-15px leading-relaxed" :class="isDark ? 'text-gray-400' : 'text-gray-500 dark:text-gray-400'" :style="bodyStyle" v-html="content" />
|
||||
</div>
|
||||
|
||||
<!-- 媒体:视频优先,其次图片;填满内容容器(宽度跟随「内容区宽度」配置),高度按素材比例自适应 -->
|
||||
<div v-if="videoUrl || mediaUrl" class="w-full">
|
||||
<video
|
||||
v-if="videoUrl"
|
||||
:src="videoUrl"
|
||||
class="w-full rounded-16px object-cover shadow-lg"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
/>
|
||||
<img v-else :src="mediaUrl" :alt="title" class="w-full rounded-16px object-cover shadow-lg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,13 +10,13 @@ const isEmbed = computed(() => /youtube\.com|youtu\.be|bilibili\.com|player\.bil
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="section-container py-96px">
|
||||
<div class="section-container py-[var(--sec-py,96px)]">
|
||||
<!-- 标题区 -->
|
||||
<div class="text-center max-w-640px mx-auto mb-48px">
|
||||
<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 text-3xl md:text-4xl font-bold" :class="isDark ? 'text-white' : 'text-gray-900 dark:text-white'" :style="titleStyle">
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user