fix(listings): 手机超宽轮播改为可横滑展示和大图预览

分站海报比例很宽,整图塞进轮播会缩成一条,点开预览也看不清。首页按固定高度横滑查看,预览用大图灯箱代替默认图片预览。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-10-05 15:02:37 +08:00
co-authored by Cursor
parent 4f69657017
commit bd502bfa63
2 changed files with 258 additions and 17 deletions
@@ -23,7 +23,7 @@ import {
type CooperationFeedbackConfig,
type HomeBannerSlide,
} from '@/features/listings/api/homeConfig'
import { showImagePreview, showToast } from 'vant'
import { showToast } from 'vant'
import AuthImage from '@/shared/components/business/AuthImage.vue'
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
import {
@@ -103,6 +103,15 @@ const displayBanners = computed<HomeBannerSlide[]>(() => {
}
return bannerSlides.value
})
const bannerSwipeRef = ref<{ prev: () => void; next: () => void } | null>(null)
const bannerPreviewVisible = ref(false)
const bannerPreviewIndex = ref(0)
const bannerPreviewScroller = ref<HTMLElement | null>(null)
const bannerPreviewImages = computed(() =>
displayBanners.value
.map(slide => slide.image_url?.trim() || '')
.filter((url): url is string => Boolean(url))
)
const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
const awRecyclePreviewVisible = ref(false)
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
@@ -396,6 +405,7 @@ onBeforeRouteLeave(() => {
onBeforeUnmount(() => {
persistScrollTop()
document.body.style.overflow = ''
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
})
@@ -510,21 +520,68 @@ function openCooperationFeedback() {
cooperationFeedbackPreviewVisible.value = true
}
let posterDragged = false
function centerWideImage(event: Event) {
const img = event.target as HTMLImageElement
const scroller = img.parentElement
if (!scroller) return
scroller.scrollLeft = Math.max(0, (scroller.scrollWidth - scroller.clientWidth) / 2)
}
function onPosterTouchStart() {
posterDragged = false
}
function onPosterTouchMove() {
posterDragged = true
}
function onPosterClick(slide: HomeBannerSlide) {
if (posterDragged) {
posterDragged = false
return
}
openBannerPreview(slide)
}
function stepBanner(step: number) {
if (step < 0) bannerSwipeRef.value?.prev()
else bannerSwipeRef.value?.next()
}
function centerBannerPreview() {
const scroller = bannerPreviewScroller.value
if (!scroller) return
scroller.scrollLeft = Math.max(0, (scroller.scrollWidth - scroller.clientWidth) / 2)
}
function openBannerPreview(slide: HomeBannerSlide) {
const url = slide.image_url?.trim()
if (!url) return
const images = displayBanners.value
.map(item => item.image_url?.trim() || '')
.filter((item): item is string => Boolean(item))
const startPosition = images.indexOf(url)
const startPosition = bannerPreviewImages.value.indexOf(url)
if (startPosition < 0) return
showImagePreview({
images,
startPosition,
closeable: true,
})
bannerPreviewIndex.value = startPosition
bannerPreviewVisible.value = true
}
function closeBannerPreview() {
bannerPreviewVisible.value = false
}
function shiftBannerPreview(step: number) {
const total = bannerPreviewImages.value.length
if (total <= 1) return
bannerPreviewIndex.value = (bannerPreviewIndex.value + step + total) % total
}
watch([bannerPreviewVisible, bannerPreviewIndex], async ([open]) => {
document.body.style.overflow = open ? 'hidden' : ''
if (!open) return
await nextTick()
centerBannerPreview()
})
function hasBannerCopy(slide: HomeBannerSlide) {
return Boolean(slide.eyebrow?.trim() || slide.title?.trim() || slide.pill?.trim())
}
@@ -915,7 +972,15 @@ if (!restoreHomeConfigFromCache()) {
</div>
<!-- Banner 轮播 -->
<van-swipe class="banner-swipe" :autoplay="3600" lazy-render>
<van-swipe
ref="bannerSwipeRef"
class="banner-swipe"
:class="{
'is-poster': displayBanners.some(slide => slide.image_url && !hasBannerCopy(slide)),
}"
:autoplay="3600"
lazy-render
>
<van-swipe-item v-for="slide in displayBanners" :key="slide.image_url || slide.title">
<div
class="mobile-banner"
@@ -924,13 +989,28 @@ if (!restoreHomeConfigFromCache()) {
{
'has-image': slide.image_url,
'has-copy': hasBannerCopy(slide),
'is-poster': !!slide.image_url && !hasBannerCopy(slide),
'is-previewable': !!slide.image_url,
},
]"
@click="openBannerPreview(slide)"
>
<div
v-if="slide.image_url && !hasBannerCopy(slide)"
class="poster-scroller"
@touchstart.stop="onPosterTouchStart"
@touchmove.stop="onPosterTouchMove"
@click.stop="onPosterClick(slide)"
>
<img
class="banner-image"
:src="slide.image_url"
:alt="slide.title || '首页轮播图'"
@load="centerWideImage"
/>
</div>
<img
v-if="slide.image_url"
v-else-if="slide.image_url"
class="banner-image"
:src="slide.image_url"
:alt="slide.title || slide.eyebrow || '首页轮播图'"
@@ -944,6 +1024,16 @@ if (!restoreHomeConfigFromCache()) {
</div>
</van-swipe-item>
</van-swipe>
<div
v-if="
displayBanners.length > 1 &&
displayBanners.some(slide => slide.image_url && !hasBannerCopy(slide))
"
class="banner-step-row"
>
<button type="button" @click="stepBanner(-1)">上一张</button>
<button type="button" @click="stepBanner(1)">下一张</button>
</div>
<div class="biz-entry-grid" aria-label="业务入口">
<RouterLink class="biz-entry" to="/">
@@ -1283,6 +1373,35 @@ if (!restoreHomeConfigFromCache()) {
</van-popup>
<MobileBottomNav />
<Teleport to="body">
<div
v-if="bannerPreviewVisible"
class="banner-lightbox"
role="dialog"
aria-modal="true"
aria-label="轮播图预览"
>
<button class="banner-lightbox-close" type="button" @click="closeBannerPreview">
关闭
</button>
<p class="banner-lightbox-count">
{{ bannerPreviewIndex + 1 }}/{{ bannerPreviewImages.length }}
</p>
<div ref="bannerPreviewScroller" class="banner-lightbox-scroller">
<img
:src="bannerPreviewImages[bannerPreviewIndex]"
alt="轮播图预览"
@load="centerBannerPreview"
/>
</div>
<p class="banner-lightbox-hint">左右滑动查看完整海报</p>
<div v-if="bannerPreviewImages.length > 1" class="banner-lightbox-nav">
<button type="button" @click="shiftBannerPreview(-1)">上一张</button>
<button type="button" @click="shiftBannerPreview(1)">下一张</button>
</div>
</div>
</Teleport>
</main>
</template>