1. 播放器歌词效果
This commit is contained in:
@@ -43,8 +43,16 @@
|
||||
:active="effectsActive"
|
||||
/>
|
||||
|
||||
<!-- 顶部滚动进度条 -->
|
||||
<div class="fixed top-0 left-0 h-[2px] bg-[#a88c6b] z-[55] transition-[width] duration-150" :style="{ width: progress * 100 + '%' }"></div>
|
||||
<!-- 顶部滚动进度条(transform 驱动,避免 width/left 强制重排) -->
|
||||
<div
|
||||
class="scroll-progress"
|
||||
aria-hidden="true"
|
||||
:style="{ '--scroll-p': String(Math.min(1, Math.max(0, progress))) }"
|
||||
>
|
||||
<div class="scroll-progress-track" />
|
||||
<div class="scroll-progress-fill" />
|
||||
<span class="scroll-progress-xi calligraphy">囍</span>
|
||||
</div>
|
||||
|
||||
<!-- 右上角:自动滚动 -->
|
||||
<div class="fixed top-6 right-5 z-50 flex flex-col items-end gap-4">
|
||||
@@ -110,11 +118,15 @@
|
||||
title="打开播放器"
|
||||
@click="openPlayerPanel"
|
||||
>
|
||||
<span class="vinyl-rim" aria-hidden="true" />
|
||||
<span class="vinyl-grooves" aria-hidden="true" />
|
||||
<span class="vinyl-sheen" aria-hidden="true" />
|
||||
<span class="vinyl-label-ring" aria-hidden="true" />
|
||||
<span
|
||||
class="vinyl-label"
|
||||
:style="audio.currentCover.value ? { backgroundImage: `url(${audio.currentCover.value})` } : undefined"
|
||||
/>
|
||||
<span class="vinyl-spindle" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -298,17 +310,17 @@
|
||||
class="page-section p-6 relative"
|
||||
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
|
||||
:style="pageHeightStyle(page)"
|
||||
:data-nine-grid="page.type === 'nine-grid' ? String(pIdx) : undefined">
|
||||
:data-gallery-hold="isHoldGalleryType(page.type) ? String(pIdx) : undefined">
|
||||
<div class="absolute inset-0 bg-white/40 -z-10"></div>
|
||||
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
||||
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
|
||||
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
|
||||
<PhotoGallery
|
||||
:page="page"
|
||||
:activate="page.type === 'nine-grid' ? !!nineGridActivate[pIdx] : false"
|
||||
:activate="isHoldGalleryType(page.type) ? !!galleryActivate[pIdx] : false"
|
||||
:nine-grid-animate="!!data.nineGridAnimate"
|
||||
@preview-image="openImagePreview"
|
||||
@ready="onNineGridReady(pIdx)"
|
||||
@ready="onGalleryReady(pIdx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -732,8 +744,11 @@ const likeBusy = ref(false)
|
||||
let likePulseTimer = null
|
||||
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
||||
const albumPreviewImages = ref([])
|
||||
const nineGridActivate = reactive({})
|
||||
const nineGridHold = ref(false)
|
||||
/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 */
|
||||
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
||||
const isHoldGalleryType = (type) => HOLD_GALLERY_TYPES.has(type)
|
||||
const galleryActivate = reactive({})
|
||||
const galleryHold = ref(false)
|
||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||
const blessingForm = reactive({ name: '', content: '', color: DEFAULT_DANMAKU_COLOR })
|
||||
const danmakuColorOptions = DANMAKU_COLORS
|
||||
@@ -773,13 +788,13 @@ const scheduleBlessingAutoClose = () => {
|
||||
|
||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
|
||||
let freeScrollLastTs = 0
|
||||
let nineGridObserver = null
|
||||
let nineGridRevealTimer = null
|
||||
const nineGridDone = new Set()
|
||||
let galleryObserver = null
|
||||
let galleryRevealTimer = null
|
||||
const galleryDone = new Set()
|
||||
const BOTTOM_RETURN_DELAY = 30000
|
||||
const FREE_SCROLL_BASE_STEP = 1.2
|
||||
/** ready 后极短缓冲再恢复滚动(串行等待已在 PhotoGallery 内完成) */
|
||||
const NINE_GRID_RESUME_DELAY_MS = 200
|
||||
const GALLERY_RESUME_DELAY_MS = 200
|
||||
|
||||
const freeInterval = computed(() => {
|
||||
const v = Number(data.value.freeScrollInterval)
|
||||
@@ -915,12 +930,25 @@ const effectsActive = computed(() =>
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
const pageScrollMax = () => Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||
/** 缓存文档高度,避免每次 scroll 读 scrollHeight 触发强制重排 */
|
||||
let cachedScrollMax = 0
|
||||
let scrollRaf = 0
|
||||
const refreshScrollMax = () => {
|
||||
cachedScrollMax = Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||
return cachedScrollMax
|
||||
}
|
||||
const pageScrollMax = () => (cachedScrollMax > 0 ? cachedScrollMax : refreshScrollMax())
|
||||
|
||||
const handleScroll = () => {
|
||||
const max = pageScrollMax()
|
||||
progress.value = max > 0 ? pageScrollTop() / max : 0
|
||||
if (isAtBottom()) scheduleBottomReturn()
|
||||
else clearBottomReturn()
|
||||
if (scrollRaf) return
|
||||
scrollRaf = requestAnimationFrame(() => {
|
||||
scrollRaf = 0
|
||||
const max = pageScrollMax()
|
||||
const top = pageScrollTop()
|
||||
progress.value = max > 0 ? top / max : 0
|
||||
if (max - top <= 8) scheduleBottomReturn()
|
||||
else clearBottomReturn()
|
||||
})
|
||||
}
|
||||
|
||||
const handleUserInteraction = () => {
|
||||
@@ -932,7 +960,10 @@ const handleUserInteraction = () => {
|
||||
}
|
||||
|
||||
const sectionEls = () => Array.from(scrollContainerRef.value?.querySelectorAll('.page-section') || [])
|
||||
const isAtBottom = () => pageScrollMax() - pageScrollTop() <= 8
|
||||
const isAtBottom = () => {
|
||||
const max = pageScrollMax()
|
||||
return max - pageScrollTop() <= 8
|
||||
}
|
||||
|
||||
const clearBottomReturn = () => {
|
||||
clearTimeout(bottomReturnTimer)
|
||||
@@ -987,45 +1018,51 @@ const canAutoAdvance = () =>
|
||||
&& !previewVisible.value
|
||||
&& !showMapOptions.value
|
||||
&& !showOpenInBrowser.value
|
||||
&& !nineGridHold.value
|
||||
&& !galleryHold.value
|
||||
|
||||
const beginNineGridGate = (pIdx) => {
|
||||
if (nineGridDone.has(pIdx)) return
|
||||
nineGridDone.add(pIdx)
|
||||
nineGridHold.value = true
|
||||
nineGridActivate[pIdx] = true
|
||||
const beginGalleryGate = (pIdx) => {
|
||||
if (galleryDone.has(pIdx)) return
|
||||
galleryDone.add(pIdx)
|
||||
galleryHold.value = true
|
||||
galleryActivate[pIdx] = true
|
||||
}
|
||||
|
||||
const onNineGridReady = (pIdx) => {
|
||||
if (!nineGridDone.has(pIdx)) return
|
||||
clearTimeout(nineGridRevealTimer)
|
||||
nineGridRevealTimer = setTimeout(() => {
|
||||
nineGridHold.value = false
|
||||
nineGridRevealTimer = null
|
||||
}, NINE_GRID_RESUME_DELAY_MS)
|
||||
const onGalleryReady = (pIdx) => {
|
||||
if (!galleryDone.has(pIdx)) return
|
||||
refreshScrollMax()
|
||||
clearTimeout(galleryRevealTimer)
|
||||
galleryRevealTimer = setTimeout(() => {
|
||||
galleryHold.value = false
|
||||
galleryRevealTimer = null
|
||||
}, GALLERY_RESUME_DELAY_MS)
|
||||
}
|
||||
|
||||
const bindNineGridObserver = () => {
|
||||
nineGridObserver?.disconnect()
|
||||
nineGridObserver = null
|
||||
const bindGalleryHoldObserver = () => {
|
||||
galleryObserver?.disconnect()
|
||||
galleryObserver = null
|
||||
if (typeof IntersectionObserver === 'undefined') return
|
||||
const nodes = scrollContainerRef.value?.querySelectorAll('[data-nine-grid]')
|
||||
const nodes = scrollContainerRef.value?.querySelectorAll('[data-gallery-hold]')
|
||||
if (!nodes?.length) return
|
||||
// 进入窗口即 activate;期间 nineGridHold 暂停自动滚
|
||||
nineGridObserver = new IntersectionObserver(
|
||||
// 区块抵达「自下而上 75%」线后再停滚+activate(= 距视口顶 25%)
|
||||
galleryObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const vh = window.innerHeight || 1
|
||||
const lineY = vh * (1 - 0.75) // 从下往上 75%
|
||||
for (const entry of entries) {
|
||||
if (!entry.isIntersecting) continue
|
||||
const raw = entry.target.getAttribute('data-nine-grid')
|
||||
const rect = entry.boundingClientRect
|
||||
// 区块跨越该水平线
|
||||
if (rect.top > lineY || rect.bottom < lineY) continue
|
||||
const raw = entry.target.getAttribute('data-gallery-hold')
|
||||
if (raw == null) continue
|
||||
const pIdx = Number(raw)
|
||||
if (!Number.isFinite(pIdx) || nineGridDone.has(pIdx)) continue
|
||||
beginNineGridGate(pIdx)
|
||||
if (!Number.isFinite(pIdx) || galleryDone.has(pIdx)) continue
|
||||
beginGalleryGate(pIdx)
|
||||
}
|
||||
},
|
||||
{ threshold: [0, 0.08, 0.2], rootMargin: '120px 0px' }
|
||||
{ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], rootMargin: '0px' }
|
||||
)
|
||||
nodes.forEach((el) => nineGridObserver.observe(el))
|
||||
nodes.forEach((el) => galleryObserver.observe(el))
|
||||
}
|
||||
|
||||
const startAutoScroll = () => {
|
||||
@@ -1045,11 +1082,12 @@ const startAutoScroll = () => {
|
||||
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
|
||||
freeScrollLastTs = now
|
||||
if (canAutoAdvance()) {
|
||||
if (isAtBottom()) {
|
||||
const top = pageScrollTop()
|
||||
const max = pageScrollMax()
|
||||
if (max - top <= 8) {
|
||||
scheduleBottomReturn()
|
||||
} else {
|
||||
window.scrollTo(0, pageScrollTop() + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
|
||||
if (isAtBottom()) scheduleBottomReturn()
|
||||
window.scrollTo(0, top + FREE_SCROLL_BASE_STEP * (dt / freeInterval.value))
|
||||
}
|
||||
}
|
||||
rafId = requestAnimationFrame(tickFreeScroll)
|
||||
@@ -1288,18 +1326,23 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
bindNineGridObserver()
|
||||
bindGalleryHoldObserver()
|
||||
refreshScrollMax()
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('resize', refreshScrollMax, { passive: true })
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove('page-scroll-snap')
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
nineGridObserver?.disconnect()
|
||||
nineGridObserver = null
|
||||
clearTimeout(nineGridRevealTimer)
|
||||
nineGridRevealTimer = null
|
||||
window.removeEventListener('resize', refreshScrollMax)
|
||||
galleryObserver?.disconnect()
|
||||
galleryObserver = null
|
||||
clearTimeout(galleryRevealTimer)
|
||||
galleryRevealTimer = null
|
||||
cancelAnimationFrame(scrollRaf)
|
||||
scrollRaf = 0
|
||||
cancelAnimationFrame(rafId)
|
||||
cancelAnimationFrame(animId)
|
||||
clearInterval(snapTimer)
|
||||
@@ -1316,7 +1359,8 @@ watch(
|
||||
() => data.value.photoPages,
|
||||
async () => {
|
||||
await nextTick()
|
||||
bindNineGridObserver()
|
||||
bindGalleryHoldObserver()
|
||||
refreshScrollMax()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -1324,7 +1368,7 @@ watch(
|
||||
() => data.value.nineGridAnimate,
|
||||
async () => {
|
||||
await nextTick()
|
||||
bindNineGridObserver()
|
||||
bindGalleryHoldObserver()
|
||||
}
|
||||
)
|
||||
watch(freeScrollSignature, restartAutoScroll)
|
||||
@@ -1486,29 +1530,105 @@ watch(
|
||||
.vinyl-wrap {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
}
|
||||
.vinyl-disc {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 999px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(60, 45, 30, 0.35);
|
||||
border: none;
|
||||
background:
|
||||
radial-gradient(circle at center, rgba(40, 30, 22, 0.2) 0 14%, transparent 15%),
|
||||
repeating-radial-gradient(circle at center, #2a2118 0 1px, #3a2f24 1px 3px);
|
||||
box-shadow: 0 6px 16px rgba(60, 45, 30, 0.22);
|
||||
radial-gradient(circle at 50% 50%, #1a1410 0 6%, transparent 6.2%),
|
||||
radial-gradient(circle at 32% 28%, rgba(255, 240, 220, 0.14) 0 10%, transparent 28%),
|
||||
repeating-radial-gradient(
|
||||
circle at center,
|
||||
#1e1813 0 0.6px,
|
||||
#2c241c 0.6px 1.4px,
|
||||
#241c16 1.4px 2.2px,
|
||||
#332a21 2.2px 3px
|
||||
);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(90, 70, 50, 0.45),
|
||||
0 0 0 2px rgba(30, 22, 16, 0.55),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
|
||||
0 7px 18px rgba(40, 28, 18, 0.32);
|
||||
}
|
||||
.vinyl-disc.spinning { animation: vinyl-spin 4.8s linear infinite; }
|
||||
.vinyl-grooves { position: absolute; inset: 0; opacity: 0.35; pointer-events: none; }
|
||||
.vinyl-rim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
box-shadow:
|
||||
inset 0 0 0 1.5px rgba(180, 150, 110, 0.22),
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
.vinyl-grooves {
|
||||
position: absolute;
|
||||
inset: 8%;
|
||||
border-radius: 999px;
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
background:
|
||||
repeating-radial-gradient(
|
||||
circle at center,
|
||||
transparent 0 1.5px,
|
||||
rgba(255, 245, 230, 0.045) 1.5px 1.9px,
|
||||
transparent 1.9px 3.4px,
|
||||
rgba(0, 0, 0, 0.18) 3.4px 3.8px
|
||||
);
|
||||
mask-image: radial-gradient(circle at center, transparent 0 26%, #000 28% 92%, transparent 94%);
|
||||
-webkit-mask-image: radial-gradient(circle at center, transparent 0 26%, #000 28% 92%, transparent 94%);
|
||||
}
|
||||
.vinyl-sheen {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
125deg,
|
||||
rgba(255, 255, 255, 0.22) 0%,
|
||||
rgba(255, 255, 255, 0) 28%,
|
||||
rgba(255, 255, 255, 0) 58%,
|
||||
rgba(255, 255, 255, 0.08) 100%
|
||||
);
|
||||
mix-blend-mode: soft-light;
|
||||
}
|
||||
.vinyl-label-ring {
|
||||
position: absolute;
|
||||
inset: 25%;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(212, 180, 140, 0.35),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.vinyl-label {
|
||||
position: absolute;
|
||||
inset: 28%;
|
||||
border-radius: 999px;
|
||||
background: #c4a484 center/cover no-repeat;
|
||||
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.25);
|
||||
background:
|
||||
radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.2), transparent 42%),
|
||||
#c4a484 center/cover no-repeat;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.28),
|
||||
0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.vinyl-spindle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin: -2.5px 0 0 -2.5px;
|
||||
border-radius: 999px;
|
||||
background: radial-gradient(circle at 35% 30%, #f0e0c8, #8a704f 70%, #5c4a35);
|
||||
box-shadow: 0 0 0 1px rgba(40, 28, 18, 0.35);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.vinyl-play-fab {
|
||||
position: absolute;
|
||||
@@ -1527,6 +1647,57 @@ watch(
|
||||
z-index: 2;
|
||||
}
|
||||
.vinyl-play-fab .fa-play { margin-left: 1px; }
|
||||
|
||||
.scroll-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
z-index: 55;
|
||||
pointer-events: none;
|
||||
}
|
||||
.scroll-progress-track {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(168, 140, 107, 0.08),
|
||||
rgba(168, 140, 107, 0.18),
|
||||
rgba(168, 140, 107, 0.08)
|
||||
);
|
||||
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.scroll-progress-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #d4b48a 0%, #a88c6b 55%, #8a704f 100%);
|
||||
box-shadow: 0 0 8px rgba(168, 140, 107, 0.45);
|
||||
transform: scaleX(var(--scroll-p, 0));
|
||||
transform-origin: left center;
|
||||
will-change: transform;
|
||||
}
|
||||
.scroll-progress-xi {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: #a88c6b;
|
||||
text-shadow:
|
||||
0 0 6px rgba(253, 251, 247, 0.95),
|
||||
0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
transform: translate3d(calc(var(--scroll-p, 0) * (100vw - 1.1em) + 0.55em - 50%), 0, 0);
|
||||
will-change: transform;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.player-panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
Reference in New Issue
Block a user