style(viewer): 广告轮播切换柔化——全帧堆叠淡入淡出
堆叠帧常驻 DOM 预加载(首轮切换不空白),活跃帧独占交互/聚焦; 恢复 10:3 高度锚点(各帧自然高度不一致会让切换时容器跳动)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,8 @@ function mountCarousel(ads: PublicAd[] = ADS): VueWrapper {
|
||||
}
|
||||
|
||||
function currentSrc(wrapper: VueWrapper): string | null {
|
||||
const img = wrapper.find('.ad-slide img').element as HTMLImageElement | undefined;
|
||||
// 全帧堆叠后当前帧=活跃帧(2026-09-29 淡入淡出改造)。
|
||||
const img = wrapper.find('.ad-slide.active img').element as HTMLImageElement | undefined;
|
||||
return img?.getAttribute('src') ?? null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { PublicAd } from '../../api/client';
|
||||
import { nextIndex, swipeStep } from '../../ads/carousel';
|
||||
@@ -22,7 +22,6 @@ const emit = defineEmits<{ close: []; imageError: [adId: string] }>();
|
||||
const { t } = useI18n();
|
||||
|
||||
const current = ref(0);
|
||||
const currentAd = computed(() => props.ads[current.value] ?? null);
|
||||
/** 指针悬停/按住期间暂停(恢复时重开完整 4s 窗口,非冻结剩余时长)。 */
|
||||
const paused = ref(false);
|
||||
|
||||
@@ -99,18 +98,25 @@ function openAd(ad: PublicAd): void {
|
||||
@pointerenter="paused = true" @pointerdown="paused = true" @pointerleave="paused = false"
|
||||
@touchstart="onTouchStart" @touchend="onTouchEnd"
|
||||
>
|
||||
<button
|
||||
v-if="currentAd !== null" type="button" class="ad-slide" :aria-label="t('ads.open')"
|
||||
@click="openAd(currentAd)"
|
||||
>
|
||||
<img
|
||||
:src="currentAd.imageUrl"
|
||||
:alt="t('ads.label')"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
@error="currentAd !== null && emit('imageError', currentAd.adId)"
|
||||
<!-- 全帧堆叠 + 透明度过渡(2026-09-29 切换柔化):所有帧常驻 DOM 预加载
|
||||
(首轮切换不空白),仅活跃帧可交互/可聚焦;错误上报只看活跃帧。 -->
|
||||
<div class="ad-slides">
|
||||
<button
|
||||
v-for="(ad, i) in ads" :key="ad.adId" type="button" class="ad-slide"
|
||||
:class="{ active: i === current }" :aria-label="t('ads.open')"
|
||||
:aria-hidden="i === current ? undefined : 'true'"
|
||||
:tabindex="i === current ? 0 : -1"
|
||||
@click="openAd(ad)"
|
||||
>
|
||||
</button>
|
||||
<img
|
||||
:src="ad.imageUrl"
|
||||
:alt="t('ads.label')"
|
||||
decoding="async"
|
||||
draggable="false"
|
||||
@error="i === current && emit('imageError', ad.adId)"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<span class="ad-badge">{{ t('ads.label') }}</span>
|
||||
<button type="button" class="ad-close" :aria-label="t('ads.close')" @click="emit('close')">✕</button>
|
||||
<nav v-if="ads.length > 1" class="ad-dots" :aria-label="t('ads.dots')">
|
||||
@@ -130,7 +136,8 @@ function openAd(ad: PublicAd): void {
|
||||
.ad-carousel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
/* aspect-ratio: 10 / 3; 建议素材 1200×360 */
|
||||
/* 堆叠帧的高度锚点(各帧自然高度不一致会让切换时容器跳动);建议素材 1200×360 */
|
||||
aspect-ratio: 10 / 3;
|
||||
margin: 16px 0;
|
||||
overflow: hidden;
|
||||
border-radius: var(--viewer-radius-img);
|
||||
@@ -140,7 +147,14 @@ function openAd(ad: PublicAd): void {
|
||||
* ThumbnailStrip pan-x 的对偶(pan-y)。 */
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.ad-slides {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.ad-slide {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -148,6 +162,15 @@ function openAd(ad: PublicAd): void {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
/* 淡入淡出(切换柔化):非活跃帧透明且不拦截交互;transition 只对 opacity,
|
||||
* 与 prefers-reduced-motion 无关(纯视觉淡变,非位移动画)。 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.45s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ad-slide.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.ad-slide img {
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user