增加多种效果
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
<!-- 胶片颗粒质感层 -->
|
||||
<div class="film-grain"></div>
|
||||
|
||||
<!-- 缓冲/加载指示器 -->
|
||||
<!-- 高级缓冲层 (Premium Loading) -->
|
||||
<transition name="fade">
|
||||
<div v-if="isBuffering" class="buffering-layer">
|
||||
<div class="spinner-border"></div>
|
||||
<div class="loader-wrapper">
|
||||
<div class="premium-loader"></div>
|
||||
<div class="loading-text">RENDERING</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
@@ -19,7 +22,7 @@
|
||||
<div v-if="showControls" class="top-bar glass-panel">
|
||||
<div class="bar-left">
|
||||
<button class="text-btn" @click="$emit('exit')">
|
||||
<i class="fas fa-arrow-left"></i> <span class="btn-text"></span>
|
||||
<i class="fas fa-arrow-left"></i> <span class="btn-text">结束放映</span>
|
||||
</button>
|
||||
<div class="meta-info">
|
||||
<h3 class="slide-title">{{ title }}</h3>
|
||||
@@ -32,7 +35,7 @@
|
||||
<i :class="isPlaying ? 'fas fa-pause' : 'fas fa-play'"></i>
|
||||
</button>
|
||||
|
||||
<!-- 全屏按钮 (外置,方便操作) -->
|
||||
<!-- 全屏按钮 -->
|
||||
<button class="icon-btn" @click.stop="$emit('toggleFullscreen')" :title="isFullscreen ? '退出全屏' : '全屏'">
|
||||
<i :class="isFullscreen ? 'fas fa-compress' : 'fas fa-expand'"></i>
|
||||
</button>
|
||||
@@ -200,16 +203,20 @@ const formatDate = (dateStr) => {
|
||||
|
||||
const getSliderPercent = (val, min, max) => ((val - min) / (max - min)) * 100
|
||||
|
||||
// --- 核心:预加载 ---
|
||||
// --- 核心:高级图片预加载器 ---
|
||||
const loadImage = (url) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image()
|
||||
img.src = url
|
||||
if (img.complete) {
|
||||
resolve()
|
||||
if (img.decode) {
|
||||
img.decode().then(() => resolve()).catch(() => resolve());
|
||||
} else {
|
||||
img.onload = () => resolve()
|
||||
img.onerror = () => resolve()
|
||||
if (img.complete) {
|
||||
resolve()
|
||||
} else {
|
||||
img.onload = () => resolve()
|
||||
img.onerror = () => resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -247,8 +254,10 @@ const handleAutoSwitch = async () => {
|
||||
const nextPhoto = props.photos[nextIdx]
|
||||
const nextUrl = getImgUrl(nextPhoto.original || nextPhoto.compressed)
|
||||
|
||||
const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 100)
|
||||
const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 50)
|
||||
|
||||
await loadImage(nextUrl)
|
||||
|
||||
clearTimeout(bufferTimeout)
|
||||
isBuffering.value = false
|
||||
|
||||
@@ -284,10 +293,15 @@ const prevSlide = async () => {
|
||||
progress.value = 0
|
||||
const prevIdx = (currentIndex.value - 1 + props.photos.length) % props.photos.length
|
||||
|
||||
isBuffering.value = true
|
||||
isSwitching = true
|
||||
const prevPhoto = props.photos[prevIdx]
|
||||
const bufferTimeout = setTimeout(() => { isBuffering.value = true }, 50)
|
||||
|
||||
await loadImage(getImgUrl(prevPhoto.original || prevPhoto.compressed))
|
||||
|
||||
clearTimeout(bufferTimeout)
|
||||
isBuffering.value = false
|
||||
isSwitching = false
|
||||
|
||||
currentIndex.value = prevIdx
|
||||
emit('update:index', currentIndex.value)
|
||||
@@ -358,23 +372,29 @@ watch(() => isPlaying.value, (val) => {
|
||||
}
|
||||
.cinematic-container:hover { cursor: default; }
|
||||
|
||||
/* Loading UI */
|
||||
.buffering-layer {
|
||||
position: absolute; inset: 0; z-index: 100;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
background: rgba(0,0,0,0.3); backdrop-filter: blur(5px);
|
||||
background: rgba(0,0,0,0.4); backdrop-filter: blur(10px);
|
||||
}
|
||||
.spinner-border {
|
||||
width: 50px; height: 50px;
|
||||
border: 4px solid rgba(255,255,255,0.1); border-top-color: #e63946; border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
.loader-wrapper { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
|
||||
.premium-loader { width: 50px; height: 50px; position: relative; }
|
||||
.premium-loader::before, .premium-loader::after {
|
||||
content: ''; position: absolute; inset: 0; border-radius: 50%;
|
||||
border: 2px solid transparent; border-top-color: #e63946;
|
||||
}
|
||||
.premium-loader::before { animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite; }
|
||||
.premium-loader::after { animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite reverse; inset: 6px; border-top-color: #fff; opacity: 0.8; }
|
||||
.loading-text { font-size: 0.7rem; letter-spacing: 3px; color: rgba(255,255,255,0.6); font-weight: 600; animation: pulse 2s infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
||||
|
||||
/* Grain & Bg */
|
||||
.film-grain {
|
||||
position: absolute; inset: 0; z-index: 5; pointer-events: none; opacity: 0.07;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.ambient-bg {
|
||||
position: absolute; top: -10%; left: -10%; width: 120%; height: 120%;
|
||||
background-size: cover; background-position: center;
|
||||
@@ -385,6 +405,7 @@ watch(() => isPlaying.value, (val) => {
|
||||
background: radial-gradient(circle at center, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.8) 100%); z-index: 2;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.top-bar {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 90px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
@@ -414,6 +435,7 @@ watch(() => isPlaying.value, (val) => {
|
||||
.icon-btn.large { width: 56px; height: 56px; font-size: 1.4rem; background: #e63946; border: none; color: #fff; }
|
||||
.icon-btn.large:hover { transform: scale(1.1); background: #ff4d5a; }
|
||||
|
||||
/* Settings */
|
||||
.custom-dropdown-trigger { position: relative; }
|
||||
.custom-dropdown-menu {
|
||||
position: absolute; top: 100%; right: 0; margin-top: 15px;
|
||||
@@ -439,6 +461,7 @@ watch(() => isPlaying.value, (val) => {
|
||||
.pill-item:hover { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
.pill-item.active { background: #e63946; color: #fff; }
|
||||
|
||||
/* Stage */
|
||||
.stage { position: absolute; inset: 0; z-index: 10; }
|
||||
.slide-layer { position: absolute; inset: 0; display: flex; justify-content: center; align-items: center; overflow: hidden; }
|
||||
.image-wrapper { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; }
|
||||
@@ -471,6 +494,7 @@ watch(() => isPlaying.value, (val) => {
|
||||
@keyframes circle-in { from { clip-path: circle(0% at 50% 50%); } to { clip-path: circle(150% at 50% 50%); } }
|
||||
@keyframes fade-out { to { opacity: 0; } }
|
||||
|
||||
/* Bottom Bar */
|
||||
.bottom-bar {
|
||||
position: absolute; bottom: 0; left: 0; right: 0; height: 100px;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, transparent 100%);
|
||||
@@ -506,4 +530,59 @@ watch(() => isPlaying.value, (val) => {
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
.fade-scale-enter-active, .fade-scale-leave-active { transition: all 0.2s; }
|
||||
.fade-scale-enter-from, .fade-scale-leave-to { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
||||
|
||||
/* Mobile Adaptation */
|
||||
@media (max-width: 768px) {
|
||||
.top-bar {
|
||||
height: 60px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.bar-left, .bar-right {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.text-btn {
|
||||
padding: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
max-width: 120px;
|
||||
}
|
||||
.slide-title {
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.slide-date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.icon-btn.large {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
height: 80px;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Hide hover-based nav hints on mobile */
|
||||
.nav-area:hover { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,10 +72,12 @@
|
||||
>
|
||||
<swiper-slide v-for="(photo, index) in photos" :key="index">
|
||||
<div class="swiper-zoom-container">
|
||||
<!-- 每个 Slide 独立的 Loading 状态 -->
|
||||
<div v-if="!imageLoadState[index]" class="slide-spinner">
|
||||
<div class="spinner-simple"></div>
|
||||
</div>
|
||||
<!-- 高级加载状态 -->
|
||||
<transition name="fade">
|
||||
<div v-if="!imageLoadState[index]" class="slide-spinner">
|
||||
<div class="premium-loader-small"></div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<img
|
||||
:src="getImgUrl(photo.original || photo.compressed)"
|
||||
@@ -117,7 +119,6 @@ const swiperModules = [EffectFade, EffectCube, EffectCoverflow, EffectCards, Nav
|
||||
const swiperKey = ref(0)
|
||||
const currentIndex = ref(props.initialIndex)
|
||||
const currentEffect = ref('slide')
|
||||
// 记录每张图片的加载状态: { [index]: boolean }
|
||||
const imageLoadState = reactive({})
|
||||
|
||||
const effectLabels = {
|
||||
@@ -209,17 +210,25 @@ onUnmounted(() => {
|
||||
background: #000; z-index: 100;
|
||||
}
|
||||
|
||||
/* Loading Spinner for individual slides */
|
||||
/* 高级 Loading 样式 (小型版) */
|
||||
.slide-spinner {
|
||||
position: absolute; inset: 0; display: flex; justify-content: center; align-items: center;
|
||||
background: transparent;
|
||||
}
|
||||
.spinner-simple {
|
||||
width: 40px; height: 40px;
|
||||
border: 3px solid rgba(255,255,255,0.1); border-top-color: #e63946;
|
||||
border-radius: 50%; animation: spin 0.8s linear infinite;
|
||||
|
||||
.premium-loader-small {
|
||||
width: 40px; height: 40px; position: relative;
|
||||
}
|
||||
.premium-loader-small::before, .premium-loader-small::after {
|
||||
content: ''; position: absolute; inset: 0; border-radius: 50%;
|
||||
border: 2px solid transparent; border-top-color: #e63946;
|
||||
}
|
||||
.premium-loader-small::before { animation: spin 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite; }
|
||||
.premium-loader-small::after { animation: spin 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite reverse; inset: 5px; border-top-color: #fff; opacity: 0.8; }
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* 保持其他样式不变 */
|
||||
.toolbar {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 70px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
@@ -250,7 +259,7 @@ onUnmounted(() => {
|
||||
|
||||
.fullscreen-swiper { width: 100%; height: 100%; }
|
||||
.swiper-slide { display: flex; justify-content: center; align-items: center; background: #000; }
|
||||
.swiper-slide img { max-width: 100%; max-height: 100%; object-fit: contain; transition: opacity 0.3s ease; }
|
||||
.swiper-slide img { max-width: 100%; max-height: 100%; object-fit: contain; transition: opacity 0.5s ease; } /* 增加图片淡入时间 */
|
||||
|
||||
:deep(.swiper-button-next), :deep(.swiper-button-prev) { color: rgba(255,255,255,0.5); transition: color 0.3s; }
|
||||
:deep(.swiper-button-next:hover), :deep(.swiper-button-prev:hover) { color: #fff; }
|
||||
@@ -280,4 +289,43 @@ onUnmounted(() => {
|
||||
.slide-down-enter-from, .slide-down-leave-to { transform: translateY(-100%); opacity: 0; }
|
||||
.fade-scale-enter-active, .fade-scale-leave-active { transition: all 0.2s; }
|
||||
.fade-scale-enter-from, .fade-scale-leave-to { opacity: 0; transform: scale(0.95) translateY(-10px); }
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.toolbar {
|
||||
height: 60px;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* 移动端隐藏文字,仅留播放图标 */
|
||||
.play-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-left, .toolbar-right {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* 隐藏左右切换箭头,移动端主要靠滑动 */
|
||||
:deep(.swiper-button-next), :deep(.swiper-button-prev) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user