1. 优化视觉效果、后端规范化
This commit is contained in:
@@ -53,8 +53,8 @@ func migrateSchema(db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initDB() *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: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: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{})
|
database, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("数据库连接失败: %v", err)
|
log.Fatalf("数据库连接失败: %v", err)
|
||||||
|
|||||||
@@ -15,19 +15,51 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错落双图 -->
|
<!-- 错落双图:固定 2 槽占位 + 串行挂载 -->
|
||||||
<div v-else-if="page.type === 'overlap'" class="overlap-wrap relative w-full mt-2">
|
<div
|
||||||
<div v-reveal="{ variant: 'left', delay: 100 }"
|
v-else-if="page.type === 'overlap'"
|
||||||
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2" :class="frameClass">
|
ref="multiRootRef"
|
||||||
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
|
class="overlap-wrap relative w-full mt-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2 overlap-slot"
|
||||||
|
:class="{ 'overlap-placeholder': shownCount < 1 }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="shownCount >= 1"
|
||||||
|
v-reveal="{ variant: 'left', immediate: true }"
|
||||||
|
class="absolute inset-0"
|
||||||
|
:class="frameClass"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="page.img1"
|
||||||
|
class="preview-img overlap-img"
|
||||||
|
decoding="async"
|
||||||
|
@click.stop="previewImage(page.img1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-reveal="{ variant: 'right', delay: 300 }"
|
<div
|
||||||
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3" :class="frameClass">
|
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3 overlap-slot"
|
||||||
<img :src="page.img2" class="preview-img" @click.stop="previewImage(page.img2)" />
|
:class="{ 'overlap-placeholder': shownCount < 2 }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="shownCount >= 2"
|
||||||
|
v-reveal="{ variant: 'right', immediate: true }"
|
||||||
|
class="absolute inset-0"
|
||||||
|
:class="frameClass"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="page.img2"
|
||||||
|
class="preview-img overlap-img"
|
||||||
|
decoding="async"
|
||||||
|
@click.stop="previewImage(page.img2)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图 decode 后再挂小图) -->
|
<!-- 一大五小:固定 6 槽占位 + 串行填内容(大图就绪后再挂小图) -->
|
||||||
<div
|
<div
|
||||||
v-else-if="page.type === 'one-large-five-small'"
|
v-else-if="page.type === 'one-large-five-small'"
|
||||||
ref="multiRootRef"
|
ref="multiRootRef"
|
||||||
@@ -74,7 +106,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 九宫格:串行挂载 + immediate(由父级 activate 触发;iOS 自观测) -->
|
<!-- 九宫格:串行挂载 + immediate(安卓 activate;iOS 近视口 IO) -->
|
||||||
<div v-else-if="page.type === 'nine-grid'" ref="multiRootRef" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
<div v-else-if="page.type === 'nine-grid'" ref="multiRootRef" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||||
<div
|
<div
|
||||||
v-for="(img, imgIdx) in (page.images || [])"
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
@@ -111,11 +143,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 瀑布流:串行挂载 + immediate -->
|
<!-- 瀑布流:固定槽位 + 串行挂载 -->
|
||||||
<div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
|
<div v-else-if="page.type === 'masonry'" ref="multiRootRef" class="g-masonry mt-6 px-1">
|
||||||
<div
|
<div
|
||||||
v-for="(img, imgIdx) in (page.images || [])"
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
:key="imgIdx"
|
:key="imgIdx"
|
||||||
|
class="g-masonry-slot"
|
||||||
|
:class="{ 'g-masonry-placeholder': imgIdx >= shownCount }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="imgIdx < shownCount"
|
v-if="imgIdx < shownCount"
|
||||||
@@ -143,11 +177,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 拍立得:串行挂载 + immediate -->
|
<!-- 拍立得:固定槽位 + 串行挂载 -->
|
||||||
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
|
<div v-else-if="page.type === 'polaroid'" ref="multiRootRef" class="g-polaroid mt-6">
|
||||||
<div
|
<div
|
||||||
v-for="(img, imgIdx) in (page.images || [])"
|
v-for="(img, imgIdx) in (page.images || [])"
|
||||||
:key="imgIdx"
|
:key="imgIdx"
|
||||||
|
class="g-polaroid-slot"
|
||||||
|
:class="{ 'g-polaroid-placeholder': imgIdx >= shownCount }"
|
||||||
|
:style="imgIdx >= shownCount ? { transform: polaroidTransform(imgIdx) } : undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="imgIdx < shownCount"
|
v-if="imgIdx < shownCount"
|
||||||
@@ -173,12 +210,14 @@ const NINE_STEP_MS = 300
|
|||||||
const NINE_SETTLE_MS = 400
|
const NINE_SETTLE_MS = 400
|
||||||
const MULTI_STEP_MS = 220
|
const MULTI_STEP_MS = 220
|
||||||
const MULTI_SETTLE_MS = 300
|
const MULTI_SETTLE_MS = 300
|
||||||
|
const OVERLAP_STEP_MS = 260
|
||||||
|
const OVERLAP_SETTLE_MS = 300
|
||||||
const OLFS_STEP_MS = 300
|
const OLFS_STEP_MS = 300
|
||||||
const OLFS_SETTLE_MS = 320
|
const OLFS_SETTLE_MS = 320
|
||||||
const OLFS_DECODE_TIMEOUT_MS = 1200
|
const OLFS_DECODE_TIMEOUT_ANDROID_MS = 1200
|
||||||
|
const OLFS_DECODE_TIMEOUT_IOS_MS = 400
|
||||||
|
|
||||||
/** 瀑布流/拍立得仍自观测;九宫格/一大五小由父级 activate+停滚驱动
|
/** 安卓:父级 activate+停滚;iOS:近视口 IO 串行(不立刻挂载) */
|
||||||
* iOS 性能足够,跳过停滚,这两种类型也走自观测(进视口自动挂载) */
|
|
||||||
const isIOSClient = (() => {
|
const isIOSClient = (() => {
|
||||||
if (typeof navigator === 'undefined') return false
|
if (typeof navigator === 'undefined') return false
|
||||||
const ua = navigator.userAgent || ''
|
const ua = navigator.userAgent || ''
|
||||||
@@ -187,14 +226,20 @@ const isIOSClient = (() => {
|
|||||||
if (platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1) return true
|
if (platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1) return true
|
||||||
return false
|
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',
|
||||||
const HOLD_ACTIVATE_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
'one-large-five-small',
|
||||||
|
'overlap',
|
||||||
|
'masonry',
|
||||||
|
'polaroid',
|
||||||
|
])
|
||||||
|
/** iOS 自观测;安卓重型版式全部由父级 activate 驱动 */
|
||||||
|
const SEQUENTIAL_SELF_TYPES = new Set(isIOSClient ? [...HOLD_ACTIVATE_TYPES] : [])
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
page: { type: Object, required: true },
|
page: { type: Object, required: true },
|
||||||
/** 九宫格/一大五小:父级停滚后设为 true 才串行挂载 */
|
/** 重型版式:父级停滚后设为 true 才串行挂载(安卓) */
|
||||||
activate: { type: Boolean, default: false },
|
activate: { type: Boolean, default: false },
|
||||||
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
/** 九宫格:开启串行 v-reveal;关闭则一次展示 */
|
||||||
nineGridAnimate: { type: Boolean, default: false },
|
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))
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
/** iOS 跳过 img.decode(手动滚动时强制 decode 易触发内存压力);仅等 load/complete */
|
||||||
const waitImgReady = (img, timeoutMs) => {
|
const waitImgReady = (img, timeoutMs) => {
|
||||||
if (!img) return Promise.resolve()
|
if (!img) return Promise.resolve()
|
||||||
const decode = () => {
|
const waitLoad = () => {
|
||||||
if (typeof img.decode === 'function') return img.decode().catch(() => {})
|
|
||||||
if (img.complete) return Promise.resolve()
|
if (img.complete) return Promise.resolve()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
img.addEventListener('load', resolve, { once: true })
|
img.addEventListener('load', resolve, { once: true })
|
||||||
img.addEventListener('error', resolve, { once: true })
|
img.addEventListener('error', resolve, { once: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const ready = isIOSClient
|
||||||
|
? waitLoad()
|
||||||
|
: (typeof img.decode === 'function' ? img.decode().catch(() => {}) : waitLoad())
|
||||||
return Promise.race([
|
return Promise.race([
|
||||||
decode(),
|
ready,
|
||||||
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -256,29 +304,25 @@ const preloadNineGridImages = () => {
|
|||||||
|
|
||||||
const runNineSequential = async () => {
|
const runNineSequential = async () => {
|
||||||
const total = (props.page.images || []).length
|
const total = (props.page.images || []).length
|
||||||
console.log('[PG] runNineSequential start, total=', total)
|
|
||||||
const { cancelled } = await seq.run({
|
const { cancelled } = await seq.run({
|
||||||
total,
|
total,
|
||||||
stepMs: NINE_STEP_MS,
|
stepMs: NINE_STEP_MS,
|
||||||
settleMs: NINE_SETTLE_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()
|
if (!cancelled) emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showNineInstant = async () => {
|
const showNineInstant = async () => {
|
||||||
console.log('[PG] showNineInstant, count=', (props.page.images || []).length)
|
|
||||||
shownCount.value = (props.page.images || []).length
|
shownCount.value = (props.page.images || []).length
|
||||||
await nextTick()
|
await nextTick()
|
||||||
emitReady()
|
emitReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一大五小:先大图并等 decode,再慢挂小图 */
|
/** 一大五小:先大图并等就绪,再慢挂小图 */
|
||||||
const runOlfsSequential = async () => {
|
const runOlfsSequential = async () => {
|
||||||
const token = ++olfsToken
|
const token = ++olfsToken
|
||||||
const total = Math.min(6, (props.page.images || []).length)
|
const total = Math.min(6, (props.page.images || []).length)
|
||||||
console.log('[PG] runOlfsSequential start, total=', total, 'token=', token)
|
|
||||||
if (!total) {
|
if (!total) {
|
||||||
emitReady()
|
emitReady()
|
||||||
return
|
return
|
||||||
@@ -286,41 +330,48 @@ const runOlfsSequential = async () => {
|
|||||||
|
|
||||||
shownCount.value = 0
|
shownCount.value = 0
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (token !== olfsToken) { console.log('[PG] olfs cancelled at start'); return }
|
if (token !== olfsToken) return
|
||||||
|
|
||||||
await sleep(OLFS_STEP_MS)
|
await sleep(OLFS_STEP_MS)
|
||||||
if (token !== olfsToken) { console.log('[PG] olfs cancelled before 1'); return }
|
if (token !== olfsToken) return
|
||||||
shownCount.value = 1
|
shownCount.value = 1
|
||||||
await nextTick()
|
await nextTick()
|
||||||
console.log('[PG] olfs shownCount=1, olfsLargeImgRef=', !!olfsLargeImgRef.value)
|
const timeout = isIOSClient ? OLFS_DECODE_TIMEOUT_IOS_MS : OLFS_DECODE_TIMEOUT_ANDROID_MS
|
||||||
await waitImgReady(olfsLargeImgRef.value, OLFS_DECODE_TIMEOUT_MS)
|
await waitImgReady(olfsLargeImgRef.value, timeout)
|
||||||
if (token !== olfsToken) { console.log('[PG] olfs cancelled after large'); return }
|
if (token !== olfsToken) return
|
||||||
|
|
||||||
for (let n = 2; n <= total; n += 1) {
|
for (let n = 2; n <= total; n += 1) {
|
||||||
await sleep(OLFS_STEP_MS)
|
await sleep(OLFS_STEP_MS)
|
||||||
if (token !== olfsToken) { console.log('[PG] olfs cancelled at n=', n); return }
|
if (token !== olfsToken) return
|
||||||
shownCount.value = n
|
shownCount.value = n
|
||||||
console.log('[PG] olfs shownCount=', n)
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token !== olfsToken) return
|
if (token !== olfsToken) return
|
||||||
await sleep(OLFS_SETTLE_MS)
|
await sleep(OLFS_SETTLE_MS)
|
||||||
console.log('[PG] olfs done')
|
|
||||||
emitReady()
|
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 runMultiSequential = async () => {
|
||||||
const type = props.page?.type
|
|
||||||
if (!SEQUENTIAL_SELF_TYPES.has(type) || multiStarted) return
|
|
||||||
multiStarted = true
|
|
||||||
const total = (props.page.images || []).length
|
const total = (props.page.images || []).length
|
||||||
await seq.run({
|
const { cancelled } = await seq.run({
|
||||||
total,
|
total,
|
||||||
stepMs: MULTI_STEP_MS,
|
stepMs: MULTI_STEP_MS,
|
||||||
settleMs: MULTI_SETTLE_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 = () => {
|
const unbindMultiObserver = () => {
|
||||||
@@ -328,14 +379,9 @@ const unbindMultiObserver = () => {
|
|||||||
multiObserver = null
|
multiObserver = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按 page.type 调度对应的串行挂载函数(iOS 上九宫格/一大五小也走此路径) */
|
/** 按 page.type 调度对应的串行挂载函数 */
|
||||||
const runSequentialByType = async () => {
|
const runSequentialByType = async () => {
|
||||||
if (multiStarted) {
|
|
||||||
console.log('[PG] runSequentialByType skip (multiStarted)')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const type = props.page?.type
|
const type = props.page?.type
|
||||||
console.log('[PG] runSequentialByType start', { type, nineGridAnimate: props.nineGridAnimate, imgCount: (props.page?.images || []).length })
|
|
||||||
if (type === 'nine-grid') {
|
if (type === 'nine-grid') {
|
||||||
if (props.nineGridAnimate) await runNineSequential()
|
if (props.nineGridAnimate) await runNineSequential()
|
||||||
else await showNineInstant()
|
else await showNineInstant()
|
||||||
@@ -345,75 +391,57 @@ const runSequentialByType = async () => {
|
|||||||
await runOlfsSequential()
|
await runOlfsSequential()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// masonry / polaroid
|
if (type === 'overlap') {
|
||||||
await runMultiSequential()
|
await runOverlapSequential()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'masonry' || type === 'polaroid') {
|
||||||
|
await runMultiSequential()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSequentialMount = async () => {
|
||||||
|
if (multiStarted || readyEmitted) return
|
||||||
|
multiStarted = true
|
||||||
|
await runSequentialByType()
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindMultiObserver = async () => {
|
const bindMultiObserver = async () => {
|
||||||
unbindMultiObserver()
|
unbindMultiObserver()
|
||||||
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
if (!SEQUENTIAL_SELF_TYPES.has(props.page?.type)) return
|
||||||
console.warn('[PG] bindMultiObserver skip, type not in set:', props.page?.type, 'isIOS:', isIOSClient)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const el = multiRootRef.value
|
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 (!el || typeof IntersectionObserver === 'undefined') {
|
||||||
if (!multiStarted) {
|
await startSequentialMount()
|
||||||
multiStarted = true
|
|
||||||
console.log('[PG] no el/IO → runSequentialByType')
|
|
||||||
runSequentialByType()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// iOS 上九宫格/一大五小:跳过观察器直接串行挂载(反正不停滚,等到进视口才挂反而突兀)
|
// iOS 重型版式:露出约 20–30% 再串行,避免页面加载即全量 decode
|
||||||
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% 再串行,避免进屏前播完
|
|
||||||
multiObserver = new IntersectionObserver(
|
multiObserver = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
for (const entry of 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
|
if (!entry.isIntersecting) continue
|
||||||
multiObserver?.disconnect()
|
multiObserver?.disconnect()
|
||||||
multiObserver = null
|
multiObserver = null
|
||||||
multiStarted = true
|
startSequentialMount()
|
||||||
console.log('[PG] IO triggered → runSequentialByType')
|
|
||||||
runSequentialByType()
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ threshold: [0, 0.1, 0.2, 0.3, 0.5], rootMargin: '0px 0px -10% 0px' }
|
{ threshold: [0, 0.1, 0.2, 0.3, 0.5], rootMargin: '0px 0px -10% 0px' }
|
||||||
)
|
)
|
||||||
multiObserver.observe(el)
|
multiObserver.observe(el)
|
||||||
console.log('[PG] observer bound')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
|
() => props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type),
|
||||||
async (on) => {
|
async (on) => {
|
||||||
if (!on || readyEmitted) return
|
if (!on) return
|
||||||
const type = props.page?.type
|
await startSequentialMount()
|
||||||
if (type === 'nine-grid') {
|
|
||||||
if (props.nineGridAnimate) await runNineSequential()
|
|
||||||
else await showNineInstant()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (type === 'one-large-five-small') {
|
|
||||||
await runOlfsSequential()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ flush: 'post' }
|
{ flush: 'post' }
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.page?.type, props.page?.images],
|
() => [props.page?.type, props.page?.images, props.page?.img1, props.page?.img2],
|
||||||
async () => {
|
async () => {
|
||||||
seq.cancel()
|
seq.cancel()
|
||||||
olfsToken += 1
|
olfsToken += 1
|
||||||
@@ -421,19 +449,15 @@ watch(
|
|||||||
multiStarted = false
|
multiStarted = false
|
||||||
shownCount.value = 0
|
shownCount.value = 0
|
||||||
readyEmitted = false
|
readyEmitted = false
|
||||||
if (props.page?.type === 'nine-grid') {
|
preloadStarted = false
|
||||||
if (!props.nineGridAnimate) preloadNineGridImages()
|
if (props.page?.type === 'nine-grid' && !props.nineGridAnimate) {
|
||||||
// iOS 自观测路径:仍需绑定观察器
|
preloadNineGridImages()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) {
|
||||||
await bindMultiObserver()
|
await bindMultiObserver()
|
||||||
|
} else if (props.activate && HOLD_ACTIVATE_TYPES.has(props.page?.type)) {
|
||||||
|
// 安卓:activate 可能已为 true,page 变更后需重新串行
|
||||||
|
await startSequentialMount()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true, flush: 'post' }
|
{ deep: true, flush: 'post' }
|
||||||
@@ -447,7 +471,6 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
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 (!props.nineGridAnimate) preloadNineGridImages()
|
||||||
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
if (SEQUENTIAL_SELF_TYPES.has(props.page?.type)) bindMultiObserver()
|
||||||
})
|
})
|
||||||
@@ -575,9 +598,35 @@ function polaroidTransform(i) {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
box-sizing: border-box;
|
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 { 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 {
|
.g-carousel-track {
|
||||||
display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
|
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-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 { 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 {
|
.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;
|
box-shadow: 0 6px 16px rgba(0,0,0,0.12); transition: transform 0.4s ease;
|
||||||
}
|
}
|
||||||
.g-polaroid-item:hover { z-index: 10; }
|
.g-polaroid-item:hover { z-index: 10; }
|
||||||
|
|||||||
@@ -748,9 +748,15 @@ const likeBusy = ref(false)
|
|||||||
let likePulseTimer = null
|
let likePulseTimer = null
|
||||||
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
|
||||||
const albumPreviewImages = ref([])
|
const albumPreviewImages = ref([])
|
||||||
/** 九宫格 / 一大五小:进视口中线后停滚再串行加载
|
/** 重型画廊:安卓进视口中线后停滚再串行加载
|
||||||
* iOS 性能足够,跳过停滚,让组件进视口自动挂载(保留滚动连续性) */
|
* iOS 跳过停滚,由 PhotoGallery 近视口 IO 串行挂载(保留滚动连续性) */
|
||||||
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
|
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 isHoldGalleryType = (type) => !isIOS && HOLD_GALLERY_TYPES.has(type)
|
||||||
const galleryActivate = reactive({})
|
const galleryActivate = reactive({})
|
||||||
const galleryHold = ref(false)
|
const galleryHold = ref(false)
|
||||||
@@ -965,6 +971,8 @@ const handleUserInteraction = () => {
|
|||||||
isInteracting.value = true
|
isInteracting.value = true
|
||||||
cancelAnimationFrame(animId)
|
cancelAnimationFrame(animId)
|
||||||
clearTimeout(pauseTimer)
|
clearTimeout(pauseTimer)
|
||||||
|
// 立刻失效,避免暂停窗口内其它路径用旧 offset 写回顶部
|
||||||
|
freeScrollOffset = -1
|
||||||
if (isAtBottom()) scheduleBottomReturn(true)
|
if (isAtBottom()) scheduleBottomReturn(true)
|
||||||
pauseTimer = setTimeout(() => {
|
pauseTimer = setTimeout(() => {
|
||||||
isInteracting.value = false
|
isInteracting.value = false
|
||||||
@@ -1099,12 +1107,12 @@ const startAutoScroll = () => {
|
|||||||
}, 4500)
|
}, 4500)
|
||||||
} else {
|
} else {
|
||||||
freeScrollLastTs = 0
|
freeScrollLastTs = 0
|
||||||
// 首次或重启动时,把 offset 同步到当前真实位置
|
|
||||||
if (freeScrollOffset < 0) freeScrollOffset = pageScrollTop()
|
|
||||||
const tickFreeScroll = (now) => {
|
const tickFreeScroll = (now) => {
|
||||||
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
|
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
|
||||||
freeScrollLastTs = now
|
freeScrollLastTs = now
|
||||||
if (canAutoAdvance()) {
|
if (canAutoAdvance()) {
|
||||||
|
// 失效后每帧推进前同步真实位置(打断/开关自动滚后都会走到这里)
|
||||||
|
if (freeScrollOffset < 0) freeScrollOffset = pageScrollTop()
|
||||||
const max = pageScrollMax()
|
const max = pageScrollMax()
|
||||||
if (max - freeScrollOffset <= 8) {
|
if (max - freeScrollOffset <= 8) {
|
||||||
scheduleBottomReturn()
|
scheduleBottomReturn()
|
||||||
@@ -1152,8 +1160,12 @@ const completeIntro = () => {
|
|||||||
|
|
||||||
const toggleAutoScroll = () => {
|
const toggleAutoScroll = () => {
|
||||||
isAutoScroll.value = !isAutoScroll.value
|
isAutoScroll.value = !isAutoScroll.value
|
||||||
if (isAutoScroll.value && isAtBottom()) scheduleBottomReturn(true)
|
if (isAutoScroll.value) {
|
||||||
if (!isAutoScroll.value) clearBottomReturn()
|
freeScrollOffset = -1
|
||||||
|
if (isAtBottom()) scheduleBottomReturn(true)
|
||||||
|
} else {
|
||||||
|
clearBottomReturn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openInvitation = () => {
|
const openInvitation = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user