1. 海报
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
DB_HOST=root:root@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local
|
||||
# DB_HOST=root:mysql_PKC65h@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local
|
||||
|
||||
ADMIN_ACCOUNT=liqi
|
||||
ADMIN_PASSWORD=qiqi991012
|
||||
ADMIN_SECRET=wedding-admin-secret-2026
|
||||
|
||||
1122
hunliji-api/520+1314特效.html
Normal file
1122
hunliji-api/520+1314特效.html
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -54,7 +54,8 @@ func migrateSchema(db *gorm.DB) {
|
||||
}
|
||||
|
||||
func initDB() *gorm.DB {
|
||||
dsn := "root:root@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
dbHost := utils.GetEnv("DB_HOST", "root:root@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local")
|
||||
dsn := dbHost
|
||||
//dsn := "root:mysql_PKC65h@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
html,
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
/* 图片异步增高时禁止浏览器滚动锚定,避免与自动滚互相拉扯产生抽搐 */
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.calligraphy {
|
||||
|
||||
@@ -255,8 +255,9 @@
|
||||
<LikeBurst ref="likeBurstRef" />
|
||||
|
||||
<!-- 主滚动区域(注意:不要加 scroll-smooth,它会劫持 JS 滚动赋值导致卡顿) -->
|
||||
<!-- iOS 自由自动滚时对该节点做 translate3d,避免每帧写 scrollTop 与系统合成器打架 -->
|
||||
<div ref="scrollContainerRef"
|
||||
class="w-full min-h-[100dvh] no-scrollbar relative"
|
||||
class="w-full min-h-[100dvh] no-scrollbar relative scroll-transform-host"
|
||||
@touchstart="handleUserInteraction" @touchmove="handleUserInteraction"
|
||||
@wheel="handleUserInteraction" @mousedown="handleUserInteraction">
|
||||
|
||||
@@ -787,41 +788,101 @@ const scheduleBlessingAutoClose = () => {
|
||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
|
||||
/** free 模式自维护的滚动偏移,避免每帧读 scrollY 触发 iOS 同步重排/撕裂 */
|
||||
let freeScrollOffset = -1
|
||||
/** 绝对时间锚点:用 elapsed 算位移,掉帧不再永久丢进度(iOS 更容易掉帧导致比安卓慢) */
|
||||
let freeScrollOriginTs = 0
|
||||
let freeScrollOriginOffset = 0
|
||||
/** 安卓:上一帧时间戳(按 dt 推进) */
|
||||
let freeScrollLastTs = 0
|
||||
/** 最近一次真正写入的位置(安卓为整数 scrollTop;iOS transform 可为小数) */
|
||||
let freeScrollLastApplied = -1
|
||||
/** 进度条节流 */
|
||||
let freeScrollProgressTick = 0
|
||||
let scrollHeightObserver = null
|
||||
/**
|
||||
* iOS 自由自动滚:用 GPU transform 平移内容,全程不写 scrollTop。
|
||||
* 每帧写 scrollTop 会与 Safari 滚动合成器冲突 → 全程抽搐。
|
||||
*/
|
||||
let iosTransformActive = false
|
||||
const BOTTOM_RETURN_DELAY = 30000
|
||||
/** 每 freeScrollInterval ms 推进的基准像素(与配置滑块联动) */
|
||||
const FREE_SCROLL_BASE_STEP = 1.2
|
||||
/** iOS 合成器对高频 scrollTo 有轻微滞后,略提速对齐安卓观感 */
|
||||
const FREE_SCROLL_IOS_BOOST = 1.22
|
||||
const FREE_SCROLL_BASE_STEP = 1.35
|
||||
/** iOS transform 提速 */
|
||||
const FREE_SCROLL_IOS_BOOST = 1.85
|
||||
/** 安卓单帧最大计入时长 */
|
||||
const FREE_SCROLL_MAX_DT = 40
|
||||
|
||||
const freeInterval = computed(() => {
|
||||
const v = Number(data.value.freeScrollInterval)
|
||||
return v >= 10 && v <= 1000 ? v : 50
|
||||
})
|
||||
/** px / ms */
|
||||
const freeScrollVelocity = computed(() => {
|
||||
const base = FREE_SCROLL_BASE_STEP / freeInterval.value
|
||||
return isIOS ? base * FREE_SCROLL_IOS_BOOST : base
|
||||
/** px / ms(安卓按真实 dt) */
|
||||
const freeScrollVelocity = computed(() => FREE_SCROLL_BASE_STEP / freeInterval.value)
|
||||
/** iOS:每帧固定推进(支持亚像素,transform 更顺) */
|
||||
const freeScrollIosStep = computed(() => {
|
||||
const perFrame = (FREE_SCROLL_BASE_STEP / freeInterval.value) * FREE_SCROLL_IOS_BOOST * (1000 / 60)
|
||||
return Math.max(0.8, Math.round(perFrame * 100) / 100)
|
||||
})
|
||||
const freeScrollSignature = computed(() => `${data.value.scrollMode}:${freeInterval.value}`)
|
||||
const invalidateFreeScrollAnchor = () => {
|
||||
freeScrollOffset = -1
|
||||
freeScrollOriginTs = 0
|
||||
freeScrollLastTs = 0
|
||||
freeScrollLastApplied = -1
|
||||
freeScrollProgressTick = 0
|
||||
}
|
||||
const restartAutoScroll = () => {
|
||||
if (snapTimer || rafId) startAutoScroll()
|
||||
}
|
||||
const scrollingRoot = () => document.scrollingElement || document.documentElement
|
||||
const isFreeAutoScrolling = () =>
|
||||
!!rafId && data.value.scrollMode !== 'snap' && isAutoScroll.value && !isInteracting.value
|
||||
|
||||
const pageScrollTopNative = () => scrollingRoot().scrollTop || window.scrollY || 0
|
||||
|
||||
const exitIosTransformScroll = () => {
|
||||
if (!iosTransformActive) return
|
||||
const el = scrollContainerRef.value
|
||||
const y = Math.max(0, freeScrollOffset >= 0 ? freeScrollOffset : 0)
|
||||
iosTransformActive = false
|
||||
document.documentElement.classList.remove('ios-free-transform-scroll', 'free-auto-scrolling')
|
||||
if (el) {
|
||||
el.style.transform = ''
|
||||
el.style.willChange = ''
|
||||
}
|
||||
scrollingRoot().scrollTop = y
|
||||
freeScrollLastApplied = y | 0
|
||||
invalidateScrollMax()
|
||||
}
|
||||
|
||||
const enterIosTransformScroll = () => {
|
||||
if (!isIOS || iosTransformActive) return
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
const y = Math.max(0, freeScrollOffset >= 0 ? freeScrollOffset : pageScrollTopNative())
|
||||
freeScrollOffset = y
|
||||
iosTransformActive = true
|
||||
document.documentElement.classList.add('ios-free-transform-scroll', 'free-auto-scrolling')
|
||||
el.style.willChange = 'transform'
|
||||
// 原生滚回顶,视觉位置交给 transform,避免双通道互抢
|
||||
scrollingRoot().scrollTop = 0
|
||||
window.scrollTo(0, 0)
|
||||
el.style.transform = `translate3d(0, ${-y}px, 0)`
|
||||
freeScrollLastApplied = y
|
||||
invalidateScrollMax()
|
||||
refreshScrollMax()
|
||||
}
|
||||
|
||||
const applyFreeScrollTop = (y) => {
|
||||
// iOS:直接写 scrollTop,减少 scrollTo 被系统平滑插值拖慢的观感
|
||||
if (isIOS) {
|
||||
const top = Math.max(0, y)
|
||||
document.documentElement.scrollTop = top
|
||||
document.body.scrollTop = top
|
||||
if (isIOS) {
|
||||
if (!iosTransformActive) enterIosTransformScroll()
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
// transform 允许亚像素,每帧直接写(GPU,不触发滚动合成)
|
||||
el.style.transform = `translate3d(0, ${-top}px, 0)`
|
||||
freeScrollLastApplied = top
|
||||
return
|
||||
}
|
||||
window.scrollTo(0, y)
|
||||
const intTop = top | 0
|
||||
if (intTop === freeScrollLastApplied) return
|
||||
freeScrollLastApplied = intTop
|
||||
scrollingRoot().scrollTop = intTop
|
||||
}
|
||||
const switchDuration = computed(() => {
|
||||
const v = Number(data.value.pageSwitchDuration)
|
||||
@@ -961,17 +1022,32 @@ const effectsActive = computed(() =>
|
||||
)
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const pageScrollTop = () => window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0
|
||||
const pageScrollTop = () => {
|
||||
if (iosTransformActive && freeScrollOffset >= 0) return freeScrollOffset
|
||||
return pageScrollTopNative()
|
||||
}
|
||||
/** 缓存文档高度,避免每次 scroll 读 scrollHeight 触发强制重排 */
|
||||
let cachedScrollMax = 0
|
||||
let scrollRaf = 0
|
||||
const viewportHeight = () => {
|
||||
const vv = window.visualViewport
|
||||
return (vv && vv.height) || window.innerHeight || document.documentElement.clientHeight || 0
|
||||
}
|
||||
const refreshScrollMax = () => {
|
||||
cachedScrollMax = Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||
if (iosTransformActive && scrollContainerRef.value) {
|
||||
cachedScrollMax = Math.max(0, scrollContainerRef.value.scrollHeight - viewportHeight())
|
||||
} else {
|
||||
const root = scrollingRoot()
|
||||
cachedScrollMax = Math.max(0, root.scrollHeight - viewportHeight())
|
||||
}
|
||||
return cachedScrollMax
|
||||
}
|
||||
const pageScrollMax = () => (cachedScrollMax > 0 ? cachedScrollMax : refreshScrollMax())
|
||||
const invalidateScrollMax = () => { cachedScrollMax = 0 }
|
||||
|
||||
const handleScroll = () => {
|
||||
// transform 自动滚 / 自由自动滚期间不读布局
|
||||
if (iosTransformActive || isFreeAutoScrolling()) return
|
||||
if (scrollRaf) return
|
||||
scrollRaf = requestAnimationFrame(() => {
|
||||
scrollRaf = 0
|
||||
@@ -984,15 +1060,15 @@ const handleScroll = () => {
|
||||
}
|
||||
|
||||
const handleUserInteraction = () => {
|
||||
// 用户插手:立刻退出 transform,把位置交还给原生滚动
|
||||
if (iosTransformActive) exitIosTransformScroll()
|
||||
isInteracting.value = true
|
||||
cancelAnimationFrame(animId)
|
||||
clearTimeout(pauseTimer)
|
||||
// 立刻失效,避免暂停窗口内其它路径用旧 offset 写回顶部
|
||||
invalidateFreeScrollAnchor()
|
||||
if (isAtBottom()) scheduleBottomReturn(true)
|
||||
pauseTimer = setTimeout(() => {
|
||||
isInteracting.value = false
|
||||
// 恢复自动滚动前,让下一帧重新同步真实滚动位置
|
||||
invalidateFreeScrollAnchor()
|
||||
}, 3000)
|
||||
}
|
||||
@@ -1025,9 +1101,9 @@ const scheduleBottomReturn = (reset = false) => {
|
||||
|
||||
const smoothScrollTo = (target, duration) => {
|
||||
cancelAnimationFrame(animId)
|
||||
const start = pageScrollTop(), dist = target - start
|
||||
if (iosTransformActive) exitIosTransformScroll()
|
||||
const start = pageScrollTopNative(), dist = target - start
|
||||
if (!dist) return
|
||||
// 标记 offset 失效,下一次 free 滚动重新从真实位置开始
|
||||
invalidateFreeScrollAnchor()
|
||||
if (duration <= 0) { window.scrollTo(0, target); return }
|
||||
const t0 = performance.now()
|
||||
@@ -1065,6 +1141,7 @@ const canAutoAdvance = () =>
|
||||
|
||||
/** 图挂载后刷新文档高度(画廊不停滚,仅校正 scrollMax) */
|
||||
const onGalleryReady = () => {
|
||||
invalidateScrollMax()
|
||||
refreshScrollMax()
|
||||
}
|
||||
|
||||
@@ -1075,6 +1152,8 @@ const startAutoScroll = () => {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
if (data.value.scrollMode === 'snap') {
|
||||
exitIosTransformScroll()
|
||||
document.documentElement.classList.remove('free-auto-scrolling')
|
||||
if (canAutoAdvance()) goNextSection()
|
||||
snapTimer = setInterval(() => {
|
||||
if (canAutoAdvance()) goNextSection()
|
||||
@@ -1083,24 +1162,39 @@ const startAutoScroll = () => {
|
||||
invalidateFreeScrollAnchor()
|
||||
const tickFreeScroll = (now) => {
|
||||
if (canAutoAdvance()) {
|
||||
// 重新锚定:从真实位置 + 当前时刻起算,掉帧用绝对时间补齐
|
||||
if (freeScrollOriginTs <= 0 || freeScrollOffset < 0) {
|
||||
freeScrollOriginOffset = pageScrollTop()
|
||||
freeScrollOriginTs = now
|
||||
freeScrollOffset = freeScrollOriginOffset
|
||||
// 重新锚定:从真实位置续滚(打断/暂停后)
|
||||
if (freeScrollOffset < 0) {
|
||||
freeScrollOffset = iosTransformActive ? Math.max(0, freeScrollLastApplied) : pageScrollTopNative()
|
||||
freeScrollLastTs = now
|
||||
freeScrollLastApplied = -1
|
||||
}
|
||||
|
||||
if (isIOS) {
|
||||
if (!iosTransformActive) enterIosTransformScroll()
|
||||
freeScrollOffset += freeScrollIosStep.value
|
||||
} else {
|
||||
const dt = Math.min(FREE_SCROLL_MAX_DT, Math.max(0, now - (freeScrollLastTs || now)))
|
||||
freeScrollLastTs = now
|
||||
freeScrollOffset += freeScrollVelocity.value * dt
|
||||
}
|
||||
|
||||
const max = pageScrollMax()
|
||||
freeScrollOffset = freeScrollOriginOffset + freeScrollVelocity.value * (now - freeScrollOriginTs)
|
||||
if (max - freeScrollOffset <= 8) {
|
||||
freeScrollOffset = max
|
||||
applyFreeScrollTop(freeScrollOffset)
|
||||
progress.value = 1
|
||||
scheduleBottomReturn()
|
||||
} else {
|
||||
applyFreeScrollTop(freeScrollOffset)
|
||||
freeScrollProgressTick += 1
|
||||
if (freeScrollProgressTick % 10 === 0 && max > 0) {
|
||||
progress.value = Math.min(1, freeScrollOffset / max)
|
||||
}
|
||||
} else if (freeScrollOriginTs > 0) {
|
||||
// 暂停期间丢弃锚点,恢复后从用户当前位置续滚
|
||||
invalidateFreeScrollAnchor()
|
||||
}
|
||||
} else {
|
||||
if (iosTransformActive) exitIosTransformScroll()
|
||||
else document.documentElement.classList.remove('free-auto-scrolling')
|
||||
if (freeScrollOffset >= 0) invalidateFreeScrollAnchor()
|
||||
}
|
||||
rafId = requestAnimationFrame(tickFreeScroll)
|
||||
}
|
||||
@@ -1118,9 +1212,23 @@ const scheduleAutoScrollStart = () => {
|
||||
}
|
||||
|
||||
const scrollPageToTop = () => {
|
||||
exitIosTransformScroll()
|
||||
window.scrollTo(0, 0)
|
||||
document.documentElement.scrollTop = 0
|
||||
document.body.scrollTop = 0
|
||||
freeScrollOffset = 0
|
||||
}
|
||||
|
||||
const toggleAutoScroll = () => {
|
||||
isAutoScroll.value = !isAutoScroll.value
|
||||
if (isAutoScroll.value) {
|
||||
invalidateFreeScrollAnchor()
|
||||
if (isAtBottom()) scheduleBottomReturn(true)
|
||||
} else {
|
||||
clearBottomReturn()
|
||||
exitIosTransformScroll()
|
||||
invalidateFreeScrollAnchor()
|
||||
}
|
||||
}
|
||||
|
||||
const completeIntro = () => {
|
||||
@@ -1138,17 +1246,6 @@ const completeIntro = () => {
|
||||
scheduleAutoScrollStart()
|
||||
}
|
||||
|
||||
const toggleAutoScroll = () => {
|
||||
isAutoScroll.value = !isAutoScroll.value
|
||||
if (isAutoScroll.value) {
|
||||
invalidateFreeScrollAnchor()
|
||||
if (isAtBottom()) scheduleBottomReturn(true)
|
||||
} else {
|
||||
clearBottomReturn()
|
||||
invalidateFreeScrollAnchor()
|
||||
}
|
||||
}
|
||||
|
||||
const openInvitation = () => {
|
||||
if (introOpening.value) return
|
||||
clearTimeout(introTimer)
|
||||
@@ -1366,14 +1463,24 @@ onMounted(async () => {
|
||||
await nextTick()
|
||||
refreshScrollMax()
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
window.addEventListener('resize', refreshScrollMax, { passive: true })
|
||||
window.addEventListener('resize', invalidateScrollMax, { passive: true })
|
||||
window.visualViewport?.addEventListener('resize', invalidateScrollMax, { passive: true })
|
||||
// 内容高度变化时再量 scrollMax,避免自动滚热路径里读 scrollHeight
|
||||
if (typeof ResizeObserver !== 'undefined' && scrollContainerRef.value) {
|
||||
scrollHeightObserver = new ResizeObserver(() => { invalidateScrollMax() })
|
||||
scrollHeightObserver.observe(scrollContainerRef.value)
|
||||
}
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.classList.remove('page-scroll-snap')
|
||||
exitIosTransformScroll()
|
||||
document.documentElement.classList.remove('page-scroll-snap', 'free-auto-scrolling', 'ios-free-transform-scroll')
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
window.removeEventListener('resize', refreshScrollMax)
|
||||
window.removeEventListener('resize', invalidateScrollMax)
|
||||
window.visualViewport?.removeEventListener('resize', invalidateScrollMax)
|
||||
scrollHeightObserver?.disconnect()
|
||||
scrollHeightObserver = null
|
||||
cancelAnimationFrame(scrollRaf)
|
||||
scrollRaf = 0
|
||||
cancelAnimationFrame(rafId)
|
||||
@@ -1418,6 +1525,21 @@ watch(
|
||||
.album-preview-registry {
|
||||
display: none;
|
||||
}
|
||||
:global(html.free-auto-scrolling) {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
:global(html.ios-free-transform-scroll),
|
||||
:global(html.ios-free-transform-scroll body) {
|
||||
overflow: hidden !important;
|
||||
height: 100% !important;
|
||||
overscroll-behavior: none;
|
||||
touch-action: pan-x pinch-zoom;
|
||||
}
|
||||
:global(html.ios-free-transform-scroll .scroll-transform-host) {
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
:global(html.page-scroll-snap) {
|
||||
scroll-snap-type: y proximity;
|
||||
scroll-behavior: auto;
|
||||
|
||||
Reference in New Issue
Block a user