1. 优化视觉效果、后端规范化

This commit is contained in:
李琦
2026-08-03 08:57:18 +08:00
parent 9efc2b21d6
commit 38558b0b43
5 changed files with 661 additions and 158 deletions

View File

@@ -505,20 +505,58 @@
<a-textarea v-model:value="page.desc" :rows="2" />
</a-form-item>
<!-- 图片上传区按版式渲染 -->
<div class="flex flex-wrap items-center justify-between gap-2 mt-2 mb-2">
<div class="text-[11px] text-[#8A8680]">
原图用于预览展示图为等比缩放后的体积不裁切
<span v-if="resizeProgress" class="text-[#a88c6b] ml-1">{{ resizeProgress }}</span>
</div>
<a-button
size="small"
type="primary"
class="!bg-[#a88c6b] hover:!bg-[#967b5c] !border-[#a88c6b]"
:loading="resizingPage"
:disabled="uploading || !pageHasOriginal(page)"
@click="batchResizeAndUpload(page)"
>
<i class="fa-solid fa-compress mr-1"></i>一键缩放并上传
</a-button>
</div>
<div class="flex flex-wrap gap-3 mt-2">
<template v-if="page.type==='single'">
<image-field v-model="page.img1" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
<div class="relative w-[96px]">
<image-field v-model="page.img1" @pick="openUpload(u => setOriginalAndClearResized(page, 'img1', u))" :uploading="uploading" />
<div v-if="page.img1Resized" class="text-center mt-1">
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
<div v-if="page.img1ResizedMeta" class="resized-meta">{{ page.img1ResizedMeta }}</div>
</div>
</div>
</template>
<template v-else-if="page.type==='overlap'">
<image-field v-model="page.img1" label="图一" @pick="openUpload(u => page.img1 = u)" :uploading="uploading" />
<image-field v-model="page.img2" label="图" @pick="openUpload(u => page.img2 = u)" :uploading="uploading" />
<div class="relative w-[96px]">
<image-field v-model="page.img1" label="图" @pick="openUpload(u => setOriginalAndClearResized(page, 'img1', u))" :uploading="uploading" />
<div v-if="page.img1Resized" class="text-center mt-1">
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
<div v-if="page.img1ResizedMeta" class="resized-meta">{{ page.img1ResizedMeta }}</div>
</div>
</div>
<div class="relative w-[96px]">
<image-field v-model="page.img2" label="图二" @pick="openUpload(u => setOriginalAndClearResized(page, 'img2', u))" :uploading="uploading" />
<div v-if="page.img2Resized" class="text-center mt-1">
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
<div v-if="page.img2ResizedMeta" class="resized-meta">{{ page.img2ResizedMeta }}</div>
</div>
</div>
</template>
<template v-else>
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative">
<div v-for="(img, iidx) in page.images" :key="iidx" class="relative w-[96px]">
<image-field v-model="page.images[iidx]"
:label="`图${iidx+1}`"
@pick="openUpload(u => page.images[iidx] = u)"
@pick="openUpload(u => setOriginalImageAt(page, iidx, u))"
:uploading="uploading" />
<div v-if="page.imagesResized?.[iidx]" class="text-center mt-1">
<div class="text-[9px] text-[#a88c6b]">展示已生成</div>
<div v-if="page.imagesResizedMeta?.[iidx]" class="resized-meta">{{ page.imagesResizedMeta[iidx] }}</div>
</div>
<button v-if="isFlexible(page.type) && page.images.length > 1"
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-[#a88c6b] text-white text-[10px] leading-none flex items-center justify-center shadow"
@click="removeImage(page, iidx)" title="移除该图"></button>
@@ -698,6 +736,7 @@ import { useAdminStore } from '@/stores/admin'
import ImageField from '@/components/ImageField.vue'
import { BORDER_STYLES, borderClass } from '@/composables/borderStyles'
import { resolveDanmakuSwatch } from '@/utils/danmakuColors'
import { getSlotResizeSpecs, resizeSlotToFile, probeResizedMeta, metaHasFileSize } from '@/composables/resizeImage'
echarts.use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer])
@@ -732,6 +771,8 @@ const activeTab = ref(readStoredTab())
const activePhotoTab = ref('0')
const saving = ref(false)
const uploading = ref(false)
const resizingPage = ref(false)
const resizeProgress = ref('')
const uploadConfigSaving = ref(false)
const tabLoading = ref(false)
const rsvpLoading = ref(false)
@@ -850,10 +891,42 @@ function onHeightFieldCustomVh(target, key, val) {
target[key] = (val || 100) + 'vh'
}
function syncImagesResized(p) {
if (!Array.isArray(p.images)) {
delete p.imagesResized
delete p.imagesResizedMeta
return
}
if (!Array.isArray(p.imagesResized)) p.imagesResized = []
if (!Array.isArray(p.imagesResizedMeta)) p.imagesResizedMeta = []
while (p.imagesResized.length < p.images.length) p.imagesResized.push('')
while (p.imagesResizedMeta.length < p.images.length) p.imagesResizedMeta.push('')
if (p.imagesResized.length > p.images.length) p.imagesResized.length = p.images.length
if (p.imagesResizedMeta.length > p.images.length) p.imagesResizedMeta.length = p.images.length
}
function ensurePageShape(p) {
if (p.type === 'single') { p.img1 = p.img1 || ''; delete p.img2; delete p.images }
else if (p.type === 'overlap') { p.img1 = p.img1 || ''; p.img2 = p.img2 || ''; delete p.images }
else {
if (p.type === 'single') {
p.img1 = p.img1 || ''
p.img1Resized = p.img1Resized || ''
p.img1ResizedMeta = p.img1ResizedMeta || ''
delete p.img2
delete p.img2Resized
delete p.img2ResizedMeta
delete p.images
delete p.imagesResized
delete p.imagesResizedMeta
} else if (p.type === 'overlap') {
p.img1 = p.img1 || ''
p.img2 = p.img2 || ''
p.img1Resized = p.img1Resized || ''
p.img2Resized = p.img2Resized || ''
p.img1ResizedMeta = p.img1ResizedMeta || ''
p.img2ResizedMeta = p.img2ResizedMeta || ''
delete p.images
delete p.imagesResized
delete p.imagesResizedMeta
} else {
// 固定数量版式:一大五小(6) / 九宫格(9);柔性版式:瀑布流/轮播/拍立得,仅首次补齐
const fixed = { 'one-large-five-small': 6, 'nine-grid': 9, 'masonry': 6, 'carousel': 5, 'polaroid': 6 }
const need = fixed[p.type] || 6
@@ -863,9 +936,15 @@ function ensurePageShape(p) {
while (p.images.length < need) p.images.push('')
if (p.images.length > need) p.images.length = need
}
delete p.img1; delete p.img2
syncImagesResized(p)
delete p.img1
delete p.img2
delete p.img1Resized
delete p.img2Resized
delete p.img1ResizedMeta
delete p.img2ResizedMeta
}
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
p.title = p.title || ''; p.subtitle = p.subtitle || ''; p.desc = p.desc || ''
p.height = p.height || '100vh'
p.borderStyle = p.borderStyle || 'mat'
// 单图宽度设置(仅 single 版式使用,逐页配置)
@@ -878,13 +957,118 @@ function ensurePageShape(p) {
function isFlexible(type) {
return ['masonry', 'carousel', 'polaroid'].includes(type)
}
function addImage(page) { if (!Array.isArray(page.images)) page.images = []; page.images.push('') }
function removeImage(page, i) { if (page.images.length > 1) page.images.splice(i, 1) }
function addImage(page) {
if (!Array.isArray(page.images)) page.images = []
page.images.push('')
syncImagesResized(page)
}
function removeImage(page, i) {
if (page.images.length > 1) {
page.images.splice(i, 1)
if (Array.isArray(page.imagesResized)) page.imagesResized.splice(i, 1)
if (Array.isArray(page.imagesResizedMeta)) page.imagesResizedMeta.splice(i, 1)
syncImagesResized(page)
}
}
// 图片排序:前移(-1) / 后移(+1)
function moveImage(page, i, dir) {
const j = i + dir
if (!Array.isArray(page.images) || j < 0 || j >= page.images.length) return
const t = page.images[i]; page.images[i] = page.images[j]; page.images[j] = t
syncImagesResized(page)
if (Array.isArray(page.imagesResized)) {
const r = page.imagesResized[i]
page.imagesResized[i] = page.imagesResized[j]
page.imagesResized[j] = r
}
if (Array.isArray(page.imagesResizedMeta)) {
const m = page.imagesResizedMeta[i]
page.imagesResizedMeta[i] = page.imagesResizedMeta[j]
page.imagesResizedMeta[j] = m
}
}
function pageHasOriginal(page) {
return getSlotResizeSpecs(page).some((s) => !!s.getOriginal())
}
function setOriginalAndClearResized(page, key, url) {
page[key] = url
page[`${key}Resized`] = ''
page[`${key}ResizedMeta`] = ''
}
function setOriginalImageAt(page, iidx, url) {
page.images[iidx] = url
syncImagesResized(page)
page.imagesResized[iidx] = ''
page.imagesResizedMeta[iidx] = ''
}
/** 缺 meta或只有像素没有文件大小时自动探测并补全 */
async function fillMissingResizedMeta(page) {
if (!page) return
if (page.type === 'single' || page.type === 'overlap') {
const keys = page.type === 'single' ? ['img1'] : ['img1', 'img2']
await Promise.all(keys.map(async (key) => {
const original = page[key]
const resized = page[`${key}Resized`]
const metaKey = `${key}ResizedMeta`
if (!resized || !original || metaHasFileSize(page[metaKey])) return
try {
page[metaKey] = await probeResizedMeta(original, resized)
} catch { /* 跨域/失效图忽略 */ }
}))
return
}
if (!Array.isArray(page.images)) return
syncImagesResized(page)
await Promise.all(page.images.map(async (original, i) => {
const resized = page.imagesResized[i]
if (!resized || !original || metaHasFileSize(page.imagesResizedMeta[i])) return
try {
page.imagesResizedMeta[i] = await probeResizedMeta(original, resized)
} catch { /* ignore */ }
}))
}
async function batchResizeAndUpload(page) {
const slots = getSlotResizeSpecs(page).filter((s) => !!s.getOriginal())
if (!slots.length) {
message.warning('请先上传原图')
return
}
resizingPage.value = true
resizeProgress.value = `0/${slots.length}`
let ok = 0
let fail = 0
try {
for (let i = 0; i < slots.length; i += 1) {
const slot = slots[i]
resizeProgress.value = `${i + 1}/${slots.length}`
try {
const { file, meta } = await resizeSlotToFile(
slot.getOriginal(),
slot.maxWidth,
slot.maxHeight,
`resized-${page.type}-${slot.key}-${Date.now()}.jpg`,
)
const res = await uploadImage(file)
if (!res?.url) throw new Error('上传无返回地址')
slot.setResized(res.url, meta)
ok += 1
} catch (err) {
fail += 1
console.warn('[resize]', slot.key, err)
}
}
if (fail === 0) message.success(`已生成 ${ok} 张展示图`)
else if (ok > 0) message.warning(`成功 ${ok} 张,失败 ${fail} 张(常见原因:跨域无法读取原图)`)
else message.error('缩放上传全部失败,请确认原图可跨域读取')
} finally {
resizingPage.value = false
resizeProgress.value = ''
}
}
// 相册页操作
@@ -1063,7 +1247,13 @@ function applySectionData(section, loaded) {
if (typeof loaded.nineGridAnimate !== 'boolean') loaded.nineGridAnimate = false
}
Object.assign(cfg, loaded)
if (section === 'photos') setActivePhotoIndex(currentPhotoIndex.value)
if (section === 'photos') {
setActivePhotoIndex(currentPhotoIndex.value)
nextTick(() => {
const page = cfg.photoPages[currentPhotoIndex.value]
if (page) fillMissingResizedMeta(page)
})
}
}
async function loadConfigSection(section) {
@@ -1281,6 +1471,16 @@ watch(activeTab, (tab) => {
onTabActivate(tab)
}, { immediate: true })
// 进入相册页 / 切换页签时,为缺 meta 的展示图自动探测尺寸
watch(
() => [activeTab.value, currentPhotoIndex.value],
([tab]) => {
if (tab !== 'photos') return
const page = cfg.photoPages[currentPhotoIndex.value]
if (page) fillMissingResizedMeta(page)
},
)
const onResize = () => { regionChart?.resize() }
onMounted(() => {
@@ -1294,6 +1494,14 @@ onUnmounted(() => {
</script>
<style scoped>
.resized-meta {
margin-top: 2px;
font-size: 9px;
line-height: 1.35;
color: #8A8680;
white-space: pre-line;
word-break: break-all;
}
.admin-tabs :deep(.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn) { color: #a88c6b; }
.admin-tabs :deep(.ant-tabs-ink-bar) { background: #a88c6b; }
.admin-tabs :deep(.ant-tabs-tab:hover .ant-tabs-tab-btn) { color: #967b5c; }

View File

@@ -309,18 +309,16 @@
<div v-for="(page, pIdx) in data.photoPages" :key="pIdx"
class="page-section p-6 relative"
:class="data.scrollMode === 'snap' ? 'snap-start' : ''"
:style="pageHeightStyle(page)"
:data-gallery-hold="isHoldGalleryType(page.type) ? String(pIdx) : undefined">
:style="pageHeightStyle(page)">
<div class="absolute inset-0 bg-white/40 -z-10"></div>
<div class="absolute top-0 left-0 w-24 h-24 border-l-[0.5px] border-t-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="absolute bottom-0 right-0 w-24 h-24 border-r-[0.5px] border-b-[0.5px] border-[#a88c6b]/30 m-6 pointer-events-none z-0"></div>
<div class="w-full h-full flex flex-col justify-center py-6 relative z-10">
<PhotoGallery
:page="page"
:activate="isHoldGalleryType(page.type) ? !!galleryActivate[pIdx] : false"
:nine-grid-animate="!!data.nineGridAnimate"
@preview-image="openImagePreview"
@ready="onGalleryReady(pIdx)"
@ready="onGalleryReady"
/>
</div>
</div>
@@ -611,9 +609,10 @@
</div>
</div>
<!-- 注意不可对子 a-image preview=false否则不会注册进 Group灯箱 src 为空 -->
<a-image-preview-group :preview="albumPreviewOptions">
<div class="album-preview-registry" aria-hidden="true">
<a-image v-for="(src, idx) in albumPreviewImages" :key="`${src}-${idx}`" :src="src" :preview="false" />
<a-image v-for="(src, idx) in albumPreviewImages" :key="`${src}-${idx}`" :src="src" />
</div>
</a-image-preview-group>
@@ -748,13 +747,6 @@ const likeBusy = ref(false)
let likePulseTimer = null
const previewVisible = ref(false), previewIndex = ref(0), albumLoaded = ref(false)
const albumPreviewImages = ref([])
/** 九宫格 / 一大五小:安卓进视口中线后停滚再串行加载
* 错落/瀑布/拍立得不停滚,由组件内 IO 分步挂载
* iOS 全部跳过停滚,近视口 IO 串行(保留滚动连续性) */
const HOLD_GALLERY_TYPES = new Set(['nine-grid', 'one-large-five-small'])
const isHoldGalleryType = (type) => !isIOS && HOLD_GALLERY_TYPES.has(type)
const galleryActivate = reactive({})
const galleryHold = ref(false)
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
const blessingForm = reactive({ name: '', content: '', color: DEFAULT_DANMAKU_COLOR })
const danmakuColorOptions = DANMAKU_COLORS
@@ -793,25 +785,44 @@ const scheduleBlessingAutoClose = () => {
}
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null, bottomReturnTimer = null
let freeScrollLastTs = 0
/** free 模式自维护的滚动偏移,避免每帧读 scrollY 触发 iOS 同步重排/撕裂 */
let freeScrollOffset = -1
let galleryObserver = null
let galleryRevealTimer = null
const galleryDone = new Set()
/** 绝对时间锚点:用 elapsed 算位移掉帧不再永久丢进度iOS 更容易掉帧导致比安卓慢) */
let freeScrollOriginTs = 0
let freeScrollOriginOffset = 0
const BOTTOM_RETURN_DELAY = 30000
/** 每 freeScrollInterval ms 推进的基准像素(与配置滑块联动) */
const FREE_SCROLL_BASE_STEP = 1.2
/** ready 后极短缓冲再恢复滚动(串行等待已在 PhotoGallery 内完成) */
const GALLERY_RESUME_DELAY_MS = 200
/** iOS 合成器对高频 scrollTo 有轻微滞后,略提速对齐安卓观感 */
const FREE_SCROLL_IOS_BOOST = 1.22
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
})
const freeScrollSignature = computed(() => `${data.value.scrollMode}:${freeInterval.value}`)
const invalidateFreeScrollAnchor = () => {
freeScrollOffset = -1
freeScrollOriginTs = 0
}
const restartAutoScroll = () => {
if (snapTimer || rafId) startAutoScroll()
}
const applyFreeScrollTop = (y) => {
// iOS直接写 scrollTop减少 scrollTo 被系统平滑插值拖慢的观感
if (isIOS) {
const top = Math.max(0, y)
document.documentElement.scrollTop = top
document.body.scrollTop = top
return
}
window.scrollTo(0, y)
}
const switchDuration = computed(() => {
const v = Number(data.value.pageSwitchDuration)
return v >= 0 && v <= 5000 ? v : 800
@@ -883,10 +894,15 @@ const normalizeImages = (images = []) => {
}
const localAlbumImages = () => {
// 灯箱优先原图;无原图时再退缩放图
const images = [data.value.heroImg, data.value.endImg]
;(data.value.photoPages || []).forEach((page) => {
images.push(page.img1, page.img2)
if (Array.isArray(page.images)) images.push(...page.images)
images.push(page.img1 || page.img1Resized, page.img2 || page.img2Resized)
if (Array.isArray(page.images)) {
page.images.forEach((src, i) => {
images.push(src || page.imagesResized?.[i])
})
}
})
return normalizeImages(images)
}
@@ -920,16 +936,21 @@ const albumPreviewOptions = computed(() => ({
}))
const openImagePreview = async (src) => {
if (!src) return
// 兼容 computed / 非字符串误传
const url = typeof src === 'string' ? src.trim() : String(src?.value || src || '').trim()
if (!url || url === '[object Object]') return
await loadAlbumPreviewImages()
let images = albumPreviewImages.value.length ? albumPreviewImages.value : [src]
let index = images.indexOf(src)
let images = albumPreviewImages.value.length ? [...albumPreviewImages.value] : []
let index = images.indexOf(url)
if (index < 0) {
images = [src, ...images]
images = [url, ...images]
albumPreviewImages.value = images
index = 0
}
previewIndex.value = index
// 等 a-image 挂载并 registerImage 后再打开,避免灯箱空白
await nextTick()
await nextTick()
previewVisible.value = true
clearBottomReturn()
cancelAnimationFrame(animId)
@@ -967,12 +988,12 @@ const handleUserInteraction = () => {
cancelAnimationFrame(animId)
clearTimeout(pauseTimer)
// 立刻失效,避免暂停窗口内其它路径用旧 offset 写回顶部
freeScrollOffset = -1
invalidateFreeScrollAnchor()
if (isAtBottom()) scheduleBottomReturn(true)
pauseTimer = setTimeout(() => {
isInteracting.value = false
// 恢复自动滚动前,让下一帧重新同步真实滚动位置
freeScrollOffset = -1
invalidateFreeScrollAnchor()
}, 3000)
}
@@ -1007,7 +1028,7 @@ const smoothScrollTo = (target, duration) => {
const start = pageScrollTop(), dist = target - start
if (!dist) return
// 标记 offset 失效,下一次 free 滚动重新从真实位置开始
freeScrollOffset = -1
invalidateFreeScrollAnchor()
if (duration <= 0) { window.scrollTo(0, target); return }
const t0 = performance.now()
const tick = (now) => {
@@ -1041,52 +1062,10 @@ const canAutoAdvance = () =>
&& !previewVisible.value
&& !showMapOptions.value
&& !showOpenInBrowser.value
&& !galleryHold.value
const beginGalleryGate = (pIdx) => {
if (galleryDone.has(pIdx)) return
galleryDone.add(pIdx)
galleryHold.value = true
galleryActivate[pIdx] = true
}
const onGalleryReady = (pIdx) => {
// iOS 跳过 hold但仍需刷新文档高度图挂载后 scrollMax 可能变化)
/** 图挂载后刷新文档高度(画廊不停滚,仅校正 scrollMax */
const onGalleryReady = () => {
refreshScrollMax()
if (!galleryDone.has(pIdx)) return
clearTimeout(galleryRevealTimer)
galleryRevealTimer = setTimeout(() => {
galleryHold.value = false
galleryRevealTimer = null
}, GALLERY_RESUME_DELAY_MS)
}
const bindGalleryHoldObserver = () => {
galleryObserver?.disconnect()
galleryObserver = null
if (typeof IntersectionObserver === 'undefined') return
const nodes = scrollContainerRef.value?.querySelectorAll('[data-gallery-hold]')
if (!nodes?.length) return
// 区块抵达「自下而上 75%」线后再停滚+activate= 距视口顶 25%
galleryObserver = new IntersectionObserver(
(entries) => {
const vh = window.innerHeight || 1
const lineY = vh * (1 - 0.9) // 从下往上 75%
for (const entry of entries) {
if (!entry.isIntersecting) continue
const rect = entry.boundingClientRect
// 区块跨越该水平线
if (rect.top > lineY || rect.bottom < lineY) continue
const raw = entry.target.getAttribute('data-gallery-hold')
if (raw == null) continue
const pIdx = Number(raw)
if (!Number.isFinite(pIdx) || galleryDone.has(pIdx)) continue
beginGalleryGate(pIdx)
}
},
{ threshold: [0, 0.1, 0.25, 0.5, 0.75, 1], rootMargin: '0px' }
)
nodes.forEach((el) => galleryObserver.observe(el))
}
const startAutoScroll = () => {
@@ -1101,21 +1080,27 @@ const startAutoScroll = () => {
if (canAutoAdvance()) goNextSection()
}, 4500)
} else {
freeScrollLastTs = 0
invalidateFreeScrollAnchor()
const tickFreeScroll = (now) => {
const dt = freeScrollLastTs ? Math.min(34, now - freeScrollLastTs) : 16.7
freeScrollLastTs = now
if (canAutoAdvance()) {
// 失效后每帧推进前同步真实位置(打断/开关自动滚后都会走到这里)
if (freeScrollOffset < 0) freeScrollOffset = pageScrollTop()
// 重新锚定:从真实位置 + 当前时刻起算,掉帧用绝对时间补齐
if (freeScrollOriginTs <= 0 || freeScrollOffset < 0) {
freeScrollOriginOffset = pageScrollTop()
freeScrollOriginTs = now
freeScrollOffset = freeScrollOriginOffset
}
const max = pageScrollMax()
freeScrollOffset = freeScrollOriginOffset + freeScrollVelocity.value * (now - freeScrollOriginTs)
if (max - freeScrollOffset <= 8) {
freeScrollOffset = max
applyFreeScrollTop(freeScrollOffset)
scheduleBottomReturn()
} else {
// 单向写入,避免 read-then-write 触发 iOS 合成器/主线程同步抖动
freeScrollOffset += FREE_SCROLL_BASE_STEP * (dt / freeInterval.value)
window.scrollTo(0, freeScrollOffset)
applyFreeScrollTop(freeScrollOffset)
}
} else if (freeScrollOriginTs > 0) {
// 暂停期间丢弃锚点,恢复后从用户当前位置续滚
invalidateFreeScrollAnchor()
}
rafId = requestAnimationFrame(tickFreeScroll)
}
@@ -1156,10 +1141,11 @@ const completeIntro = () => {
const toggleAutoScroll = () => {
isAutoScroll.value = !isAutoScroll.value
if (isAutoScroll.value) {
freeScrollOffset = -1
invalidateFreeScrollAnchor()
if (isAtBottom()) scheduleBottomReturn(true)
} else {
clearBottomReturn()
invalidateFreeScrollAnchor()
}
}
@@ -1337,7 +1323,12 @@ onMounted(async () => {
const next = { ...p }
if (next.img1) next.img1 = optimizeImageUrl(next.img1)
if (next.img2) next.img2 = optimizeImageUrl(next.img2)
if (next.img1Resized) next.img1Resized = optimizeImageUrl(next.img1Resized)
if (next.img2Resized) next.img2Resized = optimizeImageUrl(next.img2Resized)
if (Array.isArray(next.images)) next.images = next.images.map((u) => optimizeImageUrl(u))
if (Array.isArray(next.imagesResized)) {
next.imagesResized = next.imagesResized.map((u) => (u ? optimizeImageUrl(u) : u))
}
return next
})
}
@@ -1373,7 +1364,6 @@ onMounted(async () => {
}
await nextTick()
bindGalleryHoldObserver()
refreshScrollMax()
window.addEventListener('scroll', handleScroll, { passive: true })
window.addEventListener('resize', refreshScrollMax, { passive: true })
@@ -1384,10 +1374,6 @@ onUnmounted(() => {
document.documentElement.classList.remove('page-scroll-snap')
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', refreshScrollMax)
galleryObserver?.disconnect()
galleryObserver = null
clearTimeout(galleryRevealTimer)
galleryRevealTimer = null
cancelAnimationFrame(scrollRaf)
scrollRaf = 0
cancelAnimationFrame(rafId)
@@ -1406,18 +1392,10 @@ watch(
() => data.value.photoPages,
async () => {
await nextTick()
bindGalleryHoldObserver()
refreshScrollMax()
},
{ deep: true }
)
watch(
() => data.value.nineGridAnimate,
async () => {
await nextTick()
bindGalleryHoldObserver()
}
)
watch(freeScrollSignature, restartAutoScroll)
watch(
() => [isDrawerOpen.value, showBlessingDrawer.value, previewVisible.value, showMapOptions.value, showOpenInBrowser.value],