diff --git a/hunliji-api/.env b/hunliji-api/.env index fb95160..d807ac2 100644 --- a/hunliji-api/.env +++ b/hunliji-api/.env @@ -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 diff --git a/hunliji-api/520+1314特效.html b/hunliji-api/520+1314特效.html new file mode 100644 index 0000000..8f87ecd --- /dev/null +++ b/hunliji-api/520+1314特效.html @@ -0,0 +1,1122 @@ + + + + + + 抖音直播点赞爱心 · 520&1314独立特效 + + + + + + + +
+
0
+
点 赞
+
+ + +
+ ❤️ + 点击屏幕送出爱心 +
+ + +
+ + + + +
+ + + + \ No newline at end of file diff --git a/hunliji-api/hl-api b/hunliji-api/hl-api index c4f5b78..7d21827 100644 Binary files a/hunliji-api/hl-api and b/hunliji-api/hl-api differ diff --git a/hunliji-api/main.go b/hunliji-api/main.go index 38b223d..7e1c240 100644 --- a/hunliji-api/main.go +++ b/hunliji-api/main.go @@ -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 { diff --git a/vite-tailwindcss/src/style.css b/vite-tailwindcss/src/style.css index 8206ac2..bbe42df 100644 --- a/vite-tailwindcss/src/style.css +++ b/vite-tailwindcss/src/style.css @@ -15,6 +15,8 @@ html, body { font-family: var(--font-sans); + /* 图片异步增高时禁止浏览器滚动锚定,避免与自动滚互相拉扯产生抽搐 */ + overflow-anchor: none; } .calligraphy { diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index b10b7a8..70bcb5e 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -255,8 +255,9 @@ +
@@ -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 被系统平滑插值拖慢的观感 + const top = Math.max(0, y) if (isIOS) { - const top = Math.max(0, y) - document.documentElement.scrollTop = top - document.body.scrollTop = top + 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;