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独立特效
+
+
+
+
+
+
+
+
+
+
+
+ ❤️
+ 点击屏幕送出爱心
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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;