1. 海报

This commit is contained in:
李琦
2026-08-03 11:04:03 +08:00
parent 76b894c446
commit 7831e0d11a
6 changed files with 1297 additions and 47 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -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 {

View File

@@ -15,6 +15,8 @@
html,
body {
font-family: var(--font-sans);
/* 图片异步增高时禁止浏览器滚动锚定,避免与自动滚互相拉扯产生抽搐 */
overflow-anchor: none;
}
.calligraphy {

View File

@@ -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
/** 最近一次真正写入的位置(安卓为整数 scrollTopiOS 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;