diff --git a/hunliji-api/main.go b/hunliji-api/main.go index 4600cb6..1ecb537 100644 --- a/hunliji-api/main.go +++ b/hunliji-api/main.go @@ -53,8 +53,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" - dsn := "root:mysql_PKC65h@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local" + dsn := "root:root@tcp(127.0.0.1:3306)/nl_wedding?charset=utf8mb4&parseTime=True&loc=Local" + //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 { log.Fatalf("数据库连接失败: %v", err) diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 486aa75..b806f1b 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -15,19 +15,51 @@ - -
-
- + +
+
+
+ +
-
- +
+
+ +
- +
- +
- +
- +
{ if (typeof navigator === 'undefined') return false const ua = navigator.userAgent || '' @@ -187,14 +226,20 @@ const isIOSClient = (() => { if (platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1) return true return false })() -const SEQUENTIAL_SELF_TYPES = new Set( - isIOSClient ? ['masonry', 'polaroid', 'nine-grid', 'one-large-five-small'] : ['masonry', 'polaroid'] -) -const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small']) + +const HOLD_ACTIVATE_TYPES = new Set([ + 'nine-grid', + 'one-large-five-small', + 'overlap', + 'masonry', + 'polaroid', +]) +/** iOS 自观测;安卓重型版式全部由父级 activate 驱动 */ +const SEQUENTIAL_SELF_TYPES = new Set(isIOSClient ? [...HOLD_ACTIVATE_TYPES] : []) const props = defineProps({ page: { type: Object, required: true }, - /** 九宫格/一大五小:父级停滚后设为 true 才串行挂载 */ + /** 重型版式:父级停滚后设为 true 才串行挂载(安卓) */ activate: { type: Boolean, default: false }, /** 九宫格:开启串行 v-reveal;关闭则一次展示 */ nineGridAnimate: { type: Boolean, default: false }, @@ -215,18 +260,21 @@ const frameClass = computed(() => borderClass(props.page.borderStyle)) const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) +/** iOS 跳过 img.decode(手动滚动时强制 decode 易触发内存压力);仅等 load/complete */ const waitImgReady = (img, timeoutMs) => { if (!img) return Promise.resolve() - const decode = () => { - if (typeof img.decode === 'function') return img.decode().catch(() => {}) + const waitLoad = () => { if (img.complete) return Promise.resolve() return new Promise((resolve) => { img.addEventListener('load', resolve, { once: true }) img.addEventListener('error', resolve, { once: true }) }) } + const ready = isIOSClient + ? waitLoad() + : (typeof img.decode === 'function' ? img.decode().catch(() => {}) : waitLoad()) return Promise.race([ - decode(), + ready, new Promise((resolve) => setTimeout(resolve, timeoutMs)), ]) } @@ -256,29 +304,25 @@ const preloadNineGridImages = () => { const runNineSequential = async () => { const total = (props.page.images || []).length - console.log('[PG] runNineSequential start, total=', total) const { cancelled } = await seq.run({ total, stepMs: NINE_STEP_MS, settleMs: NINE_SETTLE_MS, - setCount: (n) => { console.log('[PG] nine setCount', n); shownCount.value = n }, + setCount: (n) => { shownCount.value = n }, }) - console.log('[PG] runNineSequential end, cancelled=', cancelled) if (!cancelled) emitReady() } const showNineInstant = async () => { - console.log('[PG] showNineInstant, count=', (props.page.images || []).length) shownCount.value = (props.page.images || []).length await nextTick() emitReady() } -/** 一大五小:先大图并等 decode,再慢挂小图 */ +/** 一大五小:先大图并等就绪,再慢挂小图 */ const runOlfsSequential = async () => { const token = ++olfsToken const total = Math.min(6, (props.page.images || []).length) - console.log('[PG] runOlfsSequential start, total=', total, 'token=', token) if (!total) { emitReady() return @@ -286,41 +330,48 @@ const runOlfsSequential = async () => { shownCount.value = 0 await nextTick() - if (token !== olfsToken) { console.log('[PG] olfs cancelled at start'); return } + if (token !== olfsToken) return await sleep(OLFS_STEP_MS) - if (token !== olfsToken) { console.log('[PG] olfs cancelled before 1'); return } + if (token !== olfsToken) return shownCount.value = 1 await nextTick() - console.log('[PG] olfs shownCount=1, olfsLargeImgRef=', !!olfsLargeImgRef.value) - await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS) - if (token !== olfsToken) { console.log('[PG] olfs cancelled after large'); return } + const timeout = isIOSClient ? OLFS_DECODE_TIMEOUT_IOS_MS : OLFS_DECODE_TIMEOUT_ANDROID_MS + await waitImgReady(olfsLargeImgRef.value, timeout) + if (token !== olfsToken) return for (let n = 2; n <= total; n += 1) { await sleep(OLFS_STEP_MS) - if (token !== olfsToken) { console.log('[PG] olfs cancelled at n=', n); return } + if (token !== olfsToken) return shownCount.value = n - console.log('[PG] olfs shownCount=', n) await nextTick() } if (token !== olfsToken) return await sleep(OLFS_SETTLE_MS) - console.log('[PG] olfs done') emitReady() } +/** 错落双图:两槽串行 */ +const runOverlapSequential = async () => { + const { cancelled } = await seq.run({ + total: 2, + stepMs: OVERLAP_STEP_MS, + settleMs: OVERLAP_SETTLE_MS, + setCount: (n) => { shownCount.value = n }, + }) + if (!cancelled) emitReady() +} + const runMultiSequential = async () => { - const type = props.page?.type - if (!SEQUENTIAL_SELF_TYPES.has(type) || multiStarted) return - multiStarted = true const total = (props.page.images || []).length - await seq.run({ + const { cancelled } = await seq.run({ total, stepMs: MULTI_STEP_MS, settleMs: MULTI_SETTLE_MS, - setCount: (n) => { console.log('[PG] multi setCount', n, 'type=', type); shownCount.value = n }, + setCount: (n) => { shownCount.value = n }, }) + if (!cancelled) emitReady() } const unbindMultiObserver = () => { @@ -328,14 +379,9 @@ const unbindMultiObserver = () => { multiObserver = null } -/** 按 page.type 调度对应的串行挂载函数(iOS 上九宫格/一大五小也走此路径) */ +/** 按 page.type 调度对应的串行挂载函数 */ const runSequentialByType = async () => { - if (multiStarted) { - console.log('[PG] runSequentialByType skip (multiStarted)') - return - } const type = props.page?.type - console.log('[PG] runSequentialByType start', { type, nineGridAnimate: props.nineGridAnimate, imgCount: (props.page?.images || []).length }) if (type === 'nine-grid') { if (props.nineGridAnimate) await runNineSequential() else await showNineInstant() @@ -345,75 +391,57 @@ const runSequentialByType = async () => { await runOlfsSequential() return } - // masonry / polaroid - await runMultiSequential() + if (type === 'overlap') { + await runOverlapSequential() + return + } + if (type === 'masonry' || type === 'polaroid') { + await runMultiSequential() + } +} + +const startSequentialMount = async () => { + if (multiStarted || readyEmitted) return + multiStarted = true + await runSequentialByType() } const bindMultiObserver = async () => { unbindMultiObserver() - if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) { - console.warn('[PG] bindMultiObserver skip, type not in set:', props.page?.type, 'isIOS:', isIOSClient) - return - } + if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return await nextTick() const el = multiRootRef.value - console.log('[PG] bindMultiObserver', { type: props.page?.type, hasEl: !!el, imgCount: (props.page?.images || []).length, isIOS: isIOSClient }) if (!el || typeof IntersectionObserver === 'undefined') { - if (!multiStarted) { - multiStarted = true - console.log('[PG] no el/IO → runSequentialByType') - runSequentialByType() - } + await startSequentialMount() return } - // iOS 上九宫格/一大五小:跳过观察器直接串行挂载(反正不停滚,等到进视口才挂反而突兀) - if (isIOSClient && (props.page?.type === 'nine-grid' || props.page?.type === 'one-large-five-small')) { - if (!multiStarted) { - multiStarted = true - console.log('[PG] iOS direct → runSequentialByType') - runSequentialByType() - } - return - } - // masonry/polaroid 仍用观察器:露出 30% 再串行,避免进屏前播完 + // iOS 重型版式:露出约 20–30% 再串行,避免页面加载即全量 decode multiObserver = new IntersectionObserver( (entries) => { for (const entry of entries) { - console.log('[PG] IO entry', { isIntersecting: entry.isIntersecting, ratio: entry.intersectionRatio, type: props.page?.type }) if (!entry.isIntersecting) continue multiObserver?.disconnect() multiObserver = null - multiStarted = true - console.log('[PG] IO triggered → runSequentialByType') - runSequentialByType() + startSequentialMount() break } }, { threshold: [0, 0.1, 0.2, 0.3, 0.5], rootMargin: '0px 0px -10% 0px' } ) multiObserver.observe(el) - console.log('[PG] observer bound') } watch( () => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type), async (on) => { - if (!on || readyEmitted) return - const type = props.page?.type - if (type === 'nine-grid') { - if (props.nineGridAnimate) await runNineSequential() - else await showNineInstant() - return - } - if (type === 'one-large-five-small') { - await runOlfsSequential() - } + if (!on) return + await startSequentialMount() }, { flush: 'post' } ) watch( - () => [props.page?.type, props.page?.images], + () => [props.page?.type, props.page?.images, props.page?.img1, props.page?.img2], async () => { seq.cancel() olfsToken += 1 @@ -421,19 +449,15 @@ watch( multiStarted = false shownCount.value = 0 readyEmitted = false - if (props.page?.type === 'nine-grid') { - if (!props.nineGridAnimate) preloadNineGridImages() - // iOS 自观测路径:仍需绑定观察器 - if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) await bindMultiObserver() - return - } - if (props.page?.type === 'one-large-five-small') { - // iOS 自观测路径:仍需绑定观察器 - if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) await bindMultiObserver() - return + preloadStarted = false + if (props.page?.type === 'nine-grid' && !props.nineGridAnimate) { + preloadNineGridImages() } if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) { await bindMultiObserver() + } else if (props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type)) { + // 安卓:activate 可能已为 true,page 变更后需重新串行 + await startSequentialMount() } }, { deep: true, flush: 'post' } @@ -447,7 +471,6 @@ watch( ) onMounted(() => { - console.log('[PG] onMounted', { type: props.page?.type, imgCount: (props.page?.images || []).length, isIOS: isIOSClient, inSelfSet: SEQUENTIAL_SELF_TYPES.has(props.page?.type) }) if (!props.nineGridAnimate) preloadNineGridImages() if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver() }) @@ -575,9 +598,35 @@ function polaroidTransform(i) { padding: 12px 16px; box-sizing: border-box; } +.overlap-slot { + contain: layout paint; +} +.overlap-placeholder { + visibility: hidden; + pointer-events: none; + background: transparent !important; + border: none !important; + box-shadow: none !important; +} +.overlap-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} .g-masonry { column-count: 2; column-gap: 10px; } -.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; } +.g-masonry-slot { + break-inside: avoid; + margin-bottom: 10px; + contain: layout paint; +} +.g-masonry-placeholder { + visibility: hidden; + pointer-events: none; + min-height: 120px; +} +.g-masonry-item { border-radius: 6px; } .g-carousel-track { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch; @@ -599,8 +648,17 @@ function polaroidTransform(i) { .g-dot.active { background: #a88c6b; width: 18px; border-radius: 9999px; } .g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; } +.g-polaroid-slot { + width: 42%; + contain: layout paint; +} +.g-polaroid-placeholder { + visibility: hidden; + pointer-events: none; + min-height: 160px; +} .g-polaroid-item { - width: 42%; background: #fff; padding: 8px 8px 24px; border-radius: 2px; + width: 100%; background: #fff; padding: 8px 8px 24px; border-radius: 2px; box-shadow: 0 6px 16px rgba(0,0,0,0.12); transition: transform 0.4s ease; } .g-polaroid-item:hover { z-index: 10; } diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index 005e9ae..f4f7eee 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -748,9 +748,15 @@ const likeBusy = ref(false) let likePulseTimer = null const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false) const albumPreviewImages = ref([]) -/** 九宫格 / 一大五小:进视口中线后停滚再串行加载 - * iOS 性能足够,跳过停滚,让组件进视口自动挂载(保留滚动连续性) */ -const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small']) +/** 重型画廊:安卓进视口中线后停滚再串行加载 + * iOS 跳过停滚,由 PhotoGallery 近视口 IO 串行挂载(保留滚动连续性) */ +const HOLD_GALLERY_TYPES = new Set([ + 'nine-grid', + 'one-large-five-small', + 'overlap', + 'masonry', + 'polaroid', +]) const isHoldGalleryType = (type) => !isIOS && HOLD_GALLERY_TYPES.has(type) const galleryActivate = reactive({}) const galleryHold = ref(false) @@ -965,6 +971,8 @@ const handleUserInteraction = () => { isInteracting.value = true cancelAnimationFrame(animId) clearTimeout(pauseTimer) + // 立刻失效,避免暂停窗口内其它路径用旧 offset 写回顶部 + freeScrollOffset = -1 if (isAtBottom()) scheduleBottomReturn(true) pauseTimer = setTimeout(() => { isInteracting.value = false @@ -1099,12 +1107,12 @@ const startAutoScroll = () => { }, 4500) } else { freeScrollLastTs = 0 - // 首次或重启动时,把 offset 同步到当前真实位置 - if (freeScrollOffset < 0) freeScrollOffset = pageScrollTop() const tickFreeScroll = (now) => { const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7 freeScrollLastTs = now if (canAutoAdvance()) { + // 失效后每帧推进前同步真实位置(打断/开关自动滚后都会走到这里) + if (freeScrollOffset < 0) freeScrollOffset = pageScrollTop() const max = pageScrollMax() if (max - freeScrollOffset <= 8) { scheduleBottomReturn() @@ -1152,8 +1160,12 @@ const completeIntro = () => { const toggleAutoScroll = () => { isAutoScroll.value = !isAutoScroll.value - if (isAutoScroll.value && isAtBottom()) scheduleBottomReturn(true) - if (!isAutoScroll.value) clearBottomReturn() + if (isAutoScroll.value) { + freeScrollOffset = -1 + if (isAtBottom()) scheduleBottomReturn(true) + } else { + clearBottomReturn() + } } const openInvitation = () => {