fix(listings): 手机超宽轮播改为可横滑展示和大图预览
分站海报比例很宽,整图塞进轮播会缩成一条,点开预览也看不清。首页按固定高度横滑查看,预览用大图灯箱代替默认图片预览。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -328,13 +328,135 @@
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-banner.has-image:not(.has-copy) {
|
.banner-swipe.is-poster {
|
||||||
background: #0f172a;
|
height: 168px;
|
||||||
|
background: #eef3f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-banner.has-image:not(.has-copy) .banner-image {
|
.mobile-banner.is-poster {
|
||||||
object-fit: contain;
|
height: 168px;
|
||||||
background: #0f172a;
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #eef3f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-scroller {
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-scroller::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-banner.is-poster .banner-image {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
height: 168px;
|
||||||
|
object-fit: unset;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-banner.is-poster .banner-badge {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-step-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin: -4px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-step-row button {
|
||||||
|
border: 0;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e8eef5;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(8, 12, 20, 0.94);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-close,
|
||||||
|
.banner-lightbox-nav button {
|
||||||
|
border: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(12px + env(safe-area-inset-top));
|
||||||
|
right: 12px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-count,
|
||||||
|
.banner-lightbox-hint {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-count {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(18px + env(safe-area-inset-top));
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-scroller {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-scroller img {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
height: 58vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-hint {
|
||||||
|
margin-top: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-lightbox-nav button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-banner.has-image.has-copy::after {
|
.mobile-banner.has-image.has-copy::after {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
type CooperationFeedbackConfig,
|
type CooperationFeedbackConfig,
|
||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from '@/features/listings/api/homeConfig'
|
} from '@/features/listings/api/homeConfig'
|
||||||
import { showImagePreview, showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
|
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
|
||||||
import {
|
import {
|
||||||
@@ -103,6 +103,15 @@ const displayBanners = computed<HomeBannerSlide[]>(() => {
|
|||||||
}
|
}
|
||||||
return bannerSlides.value
|
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 awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
|
||||||
const awRecyclePreviewVisible = ref(false)
|
const awRecyclePreviewVisible = ref(false)
|
||||||
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
|
const cooperationFeedback = ref<CooperationFeedbackConfig>({ ...defaultCooperationFeedbackConfig })
|
||||||
@@ -396,6 +405,7 @@ onBeforeRouteLeave(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
persistScrollTop()
|
persistScrollTop()
|
||||||
|
document.body.style.overflow = ''
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -510,21 +520,68 @@ function openCooperationFeedback() {
|
|||||||
cooperationFeedbackPreviewVisible.value = true
|
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) {
|
function openBannerPreview(slide: HomeBannerSlide) {
|
||||||
const url = slide.image_url?.trim()
|
const url = slide.image_url?.trim()
|
||||||
if (!url) return
|
if (!url) return
|
||||||
const images = displayBanners.value
|
const startPosition = bannerPreviewImages.value.indexOf(url)
|
||||||
.map(item => item.image_url?.trim() || '')
|
|
||||||
.filter((item): item is string => Boolean(item))
|
|
||||||
const startPosition = images.indexOf(url)
|
|
||||||
if (startPosition < 0) return
|
if (startPosition < 0) return
|
||||||
showImagePreview({
|
bannerPreviewIndex.value = startPosition
|
||||||
images,
|
bannerPreviewVisible.value = true
|
||||||
startPosition,
|
|
||||||
closeable: 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) {
|
function hasBannerCopy(slide: HomeBannerSlide) {
|
||||||
return Boolean(slide.eyebrow?.trim() || slide.title?.trim() || slide.pill?.trim())
|
return Boolean(slide.eyebrow?.trim() || slide.title?.trim() || slide.pill?.trim())
|
||||||
}
|
}
|
||||||
@@ -915,7 +972,15 @@ if (!restoreHomeConfigFromCache()) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner 轮播 -->
|
<!-- 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">
|
<van-swipe-item v-for="slide in displayBanners" :key="slide.image_url || slide.title">
|
||||||
<div
|
<div
|
||||||
class="mobile-banner"
|
class="mobile-banner"
|
||||||
@@ -924,13 +989,28 @@ if (!restoreHomeConfigFromCache()) {
|
|||||||
{
|
{
|
||||||
'has-image': slide.image_url,
|
'has-image': slide.image_url,
|
||||||
'has-copy': hasBannerCopy(slide),
|
'has-copy': hasBannerCopy(slide),
|
||||||
|
'is-poster': !!slide.image_url && !hasBannerCopy(slide),
|
||||||
'is-previewable': !!slide.image_url,
|
'is-previewable': !!slide.image_url,
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
@click="openBannerPreview(slide)"
|
@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
|
<img
|
||||||
v-if="slide.image_url"
|
v-else-if="slide.image_url"
|
||||||
class="banner-image"
|
class="banner-image"
|
||||||
:src="slide.image_url"
|
:src="slide.image_url"
|
||||||
:alt="slide.title || slide.eyebrow || '首页轮播图'"
|
:alt="slide.title || slide.eyebrow || '首页轮播图'"
|
||||||
@@ -944,6 +1024,16 @@ if (!restoreHomeConfigFromCache()) {
|
|||||||
</div>
|
</div>
|
||||||
</van-swipe-item>
|
</van-swipe-item>
|
||||||
</van-swipe>
|
</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="业务入口">
|
<div class="biz-entry-grid" aria-label="业务入口">
|
||||||
<RouterLink class="biz-entry" to="/">
|
<RouterLink class="biz-entry" to="/">
|
||||||
@@ -1283,6 +1373,35 @@ if (!restoreHomeConfigFromCache()) {
|
|||||||
</van-popup>
|
</van-popup>
|
||||||
|
|
||||||
<MobileBottomNav />
|
<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>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user