<template>
|
<view class="page">
|
<view class="custom-nav" :class="isSolidNav ? 'is-solid' : ''"><view class="nav-bg"></view><view class="safe-space" :style="'height:' + navTop + 'px'"></view><view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'"><view class="back-button" @tap="goBack"><image src="/static/food-assets/arrow-left-white.svg"></image></view><text class="nav-title">住--舒心安居</text><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view>
|
<scroll-view class="page-scroll" scroll-y lower-threshold="80" @scroll="handleScroll" @scrolltolower="loadMore"><view class="hero"><image class="hero-image" :src="currentScene.image" mode="aspectFill" @error="replaceHeroImage"></image><view class="hero-shade"></view><text v-if="currentScene.name != ''" class="scene-name">{{currentScene.name}}</text><text v-if="scenes.length == 0" class="no-gallery">暂无图册内容</text><scroll-view v-else class="scene-scroll" scroll-x scroll-with-animation :scroll-left="sceneScrollLeft" @scrollend="handleSceneScrollEnd"><view class="scene-row" :style="'width:' + sceneTrackWidth + 'rpx'"><view class="scene-pad"></view><view class="scene-item" v-for="(item,index) in scenes" :key="item.id" :class="index == activeIndex ? 'active' : ''" @tap="selectScene(index)"><image :src="item.image" mode="aspectFill" @error="replaceSceneImage(index)"></image></view><view class="scene-pad"></view></view></scroll-view></view>
|
<view v-if="topicDescription != ''" class="topic-summary"><view class="intro-card"><text>{{topicDescription}}</text></view></view>
|
<view class="service-section"><view class="service-tabs"><view :class="activeTab == 'hotel' ? 'active' : ''" @tap="switchTab('hotel')"><text>酒店服务</text><view></view></view><view :class="activeTab == 'apartment' ? 'active' : ''" @tap="switchTab('apartment')"><text>公寓租赁服务</text><view></view></view></view><view v-if="initialLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载住宿服务</text></view><view v-else-if="initialError" class="list-state error-state"><text>住宿服务加载失败</text><text class="retry-text" @tap="retryInitialLoad">重新加载</text></view><view v-else-if="hasLoaded && visibleServices.length == 0" class="list-state"><text>{{activeTab == 'hotel' ? '暂无相关酒店服务' : '暂无相关公寓租赁服务'}}</text></view><view v-else-if="hasLoaded"><view class="service-item" :class="item.available ? '' : 'is-unavailable'" v-for="item in visibleServices" :key="item.id" @tap="openService(item)"><image class="service-image" :src="item.image" mode="aspectFill" @error="replaceServiceImage(item)"></image><view class="service-info"><text class="service-name">{{item.name}}</text><text class="discount-tag">{{item.available ? item.discount : '已下架'}}</text><view class="address"><image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text></view></view></view></view><view v-if="loadingMore" class="list-state compact"><view class="loading-spinner"></view><text>正在加载</text></view><view v-else-if="loadMoreError" class="list-state compact load-error" @tap="retryLoadMore"><text>加载失败,点击重试</text></view><view v-else-if="hasLoaded && !hasMore && visibleServices.length > 0" class="no-more"><text>已经到底了</text></view></view><view class="bottom-space"></view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { isApiConfigured } from '../../config/api.uts'
|
import { getStayTopic, getStayTopicMerchants } from '../../services/portal.uts'
|
import { resolveApiUrl } from '../../services/http.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
|
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
|
const isSolidNav = ref(false); const windowWidth = ref(375); const fallbackImage = '/static/stay-assets/hotel-vienna.jpg'
|
const activeIndex = ref(-1); const sceneScrollLeft = ref(0); const sceneTrackWidth = ref(644); const scenes = ref<any[]>([]); const currentScene = ref({ id: 'fallback', name: '', image: fallbackImage }); const topicDescription = ref('')
|
const activeTab = ref('hotel'); const visibleServices = ref<any[]>([]); const initialLoading = ref(true); const initialError = ref(false); const loadingMore = ref(false); const loadMoreError = ref(false); const hasLoaded = ref(false); const hasMore = ref(false); const currentPage = ref(0); const listRequestVersion = ref(0); const failInitialOnce = ref(false); const failLoadMoreOnce = ref(false)
|
|
onLoad((options: any) => {
|
// #ifdef MP-WEIXIN
|
const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); windowWidth.value = info.windowWidth; navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right
|
// #endif
|
if (options != null && options['previewState'] != null) { const state = options['previewState'] as string; failInitialOnce.value = state == 'initialError'; failLoadMoreOnce.value = state == 'loadMoreError' }
|
loadStayTopic(); loadInitialServices()
|
})
|
const loadStayTopic = () => {
|
if (!isApiConfigured()) return
|
getStayTopic((data: any) => { topicDescription.value = data != null && data.description != null ? data.description as string : ''; const records: any[] = data != null && data.images instanceof Array ? data.images as any[] : []; const next: any[] = []; for (let index = 0; index < records.length; index++) { const item = records[index]; if (item.imageUrl != null && item.imageUrl != '') next.push({ id: '' + item.id, name: item.name != null ? item.name as string : '', image: resolveApiUrl(item.imageUrl as string) }) }; scenes.value = next; sceneTrackWidth.value = 644 + next.length * 137; if (next.length > 0) selectScene(Math.min(2, next.length - 1)) }, () => { scenes.value = []; topicDescription.value = '' })
|
}
|
const loadInitialServices = () => { listRequestVersion.value++; currentPage.value = 0; visibleServices.value = []; hasMore.value = false; initialLoading.value = true; initialError.value = false; loadMoreError.value = false; hasLoaded.value = false; loadStayServices(false) }
|
const retryInitialLoad = () => loadInitialServices(); const retryLoadMore = () => loadMore()
|
const selectScene = (index: number) => { if (index < 0 || index >= scenes.value.length) return; activeIndex.value = index; currentScene.value = scenes.value[index]; sceneScrollLeft.value = index * 137 * windowWidth.value / 750 }
|
const handleSceneScrollEnd = (event: any) => { let index = Math.round(event.detail.scrollLeft * 750 / windowWidth.value / 137); if (index < 0) index = 0; if (index >= scenes.value.length) index = scenes.value.length - 1; selectScene(index) }
|
const handleScroll = (event: any) => { isSolidNav.value = event.detail.scrollTop > 64 }
|
const switchTab = (tab: string) => { if (activeTab.value == tab && hasLoaded.value) return; activeTab.value = tab; loadInitialServices() }
|
const loadStayServices = (append: boolean) => {
|
if (!isApiConfigured()) { initialLoading.value = false; loadingMore.value = false; initialError.value = true; return }
|
const requestVersion = listRequestVersion.value
|
if (!append && failInitialOnce.value) { failInitialOnce.value = false; initialLoading.value = false; initialError.value = true; return }
|
if (append && failLoadMoreOnce.value) { failLoadMoreOnce.value = false; loadingMore.value = false; loadMoreError.value = true; return }
|
getStayTopicMerchants(activeTab.value, currentPage.value + 1, 4, (data: any) => { if (requestVersion != listRequestVersion.value) return; const records: any[] = data != null && data.records instanceof Array ? data.records as any[] : []; const next = append ? visibleServices.value.slice() : []; for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.id, merchantType: item.merchantType != null ? item.merchantType as string : activeTab.value, name: item.merchantName != null ? item.merchantName as string : '住宿服务', discount: item.discountText != null ? item.discountText as string : '人才专享', address: item.address != null ? item.address as string : '', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, available: item.available !== false }) }; visibleServices.value = next; currentPage.value++; hasMore.value = data != null && data.total != null ? next.length < data.total : false; initialLoading.value = false; loadingMore.value = false; hasLoaded.value = true }, () => { if (requestVersion != listRequestVersion.value) return; initialLoading.value = false; loadingMore.value = false; if (append) loadMoreError.value = true; else initialError.value = true })
|
}
|
const loadMore = () => { if (initialLoading.value || loadingMore.value || !hasMore.value) return; loadMoreError.value = false; loadingMore.value = true; loadStayServices(true) }
|
const replaceHeroImage = () => { currentScene.value.image = fallbackImage }; const replaceSceneImage = (index: number) => { if (index >= 0 && index < scenes.value.length) scenes.value[index].image = fallbackImage }; const replaceServiceImage = (item: any) => { item.image = fallbackImage }
|
const goBack = () => safeBack('/pages/benefits/benefits'); const openService = (item: any) => { if (!item.available) { uni.showToast({ title: '该服务已下架', icon: 'none' }); return }; uni.navigateTo({ url: '/pages/merchant/merchant?merchantType=' + encodeURIComponent(item.merchantType) + '&merchantId=' + encodeURIComponent(item.id) + '&name=' + encodeURIComponent(item.name) + '&image=' + encodeURIComponent(item.image) + '&address=' + encodeURIComponent(item.address) + '&discount=' + encodeURIComponent(item.discount) }) }
|
</script>
|
|
<style>
|
@font-face { font-family:'FoodCalligraphy'; src:url('/static/fonts/food-calligraphy-subset.ttf') format('truetype'); }.page,.page-scroll{width:100%;height:100vh;background:#FFF}.custom-nav{position:fixed;z-index:20;left:0;right:0;top:0;padding-bottom:20rpx}.nav-bg{position:absolute;inset:0;opacity:0;background:#F26A38}.custom-nav.is-solid .nav-bg{opacity:1}.safe-space,.nav-row{position:relative;z-index:1}.safe-space{height:28px}.nav-row{display:flex;height:32px;box-sizing:border-box;padding-left:24rpx;padding-right:10px;align-items:center;justify-content:space-between}.back-button{display:flex;width:54rpx;height:54rpx;border-radius:50%;background:rgba(45,45,45,.52);align-items:center;justify-content:center}.custom-nav.is-solid .back-button{background:transparent}.back-button image{width:38rpx;height:38rpx}.nav-title{position:absolute;left:0;right:0;color:#FFF;font-size:31rpx;text-align:center;text-shadow:0 2rpx 8rpx rgba(0,0,0,.35)}.capsule-space{width:88px;height:100%;flex-shrink:0}.hero{height:730rpx;position:relative;overflow:visible;background:#1D315D}.hero-image{position:absolute;inset:0;width:100%;height:730rpx}.hero-shade{position:absolute;left:0;right:0;top:0;height:360rpx;background:linear-gradient(180deg,rgba(0,0,0,.58),transparent)}.scene-name{position:absolute;z-index:2;left:38rpx;top:300rpx;color:#FFF;font-family:'FoodCalligraphy';font-size:48rpx;text-shadow:0 3rpx 10rpx rgba(0,0,0,.55)}.no-gallery{position:absolute;z-index:2;top:480rpx;left:0;right:0;color:#FFF;text-align:center}.scene-scroll{position:absolute;z-index:4;left:0;right:0;top:469rpx;height:150rpx;white-space:nowrap}.scene-row{display:flex;height:150rpx;align-items:center}.scene-pad{width:322rpx;flex-shrink:0}.scene-item{width:106rpx;height:106rpx;margin-right:31rpx;overflow:hidden;border:3rpx solid rgba(255,255,255,.9);border-radius:8rpx;flex-shrink:0}.scene-item image{width:100%;height:100%}.scene-item.active{border:4rpx solid #FFD329;transform:scale(1.28)}.topic-summary{margin-top:-66rpx;position:relative;z-index:4}.intro-card{margin:0 31rpx;padding:25rpx 31rpx;border-radius:26rpx;background:linear-gradient(90deg,#FF5320,#FF8A22)}.intro-card text{color:#FFF;font-size:23rpx;line-height:34rpx}.service-section{padding:48rpx 31rpx 0}.service-tabs{display:flex;height:72rpx}.service-tabs>view{display:flex;margin-right:42rpx;flex-direction:column;align-items:center}.service-tabs text{color:#8A8D92;font-size:31rpx;font-weight:600}.service-tabs .active text{color:#363636}.service-tabs view view{width:0;height:7rpx;margin-top:12rpx;border-radius:4rpx;background:linear-gradient(90deg,#FF4D36,#FFBF3A)}.service-tabs .active view{width:61rpx}.service-item{display:flex;min-height:205rpx;margin-bottom:20rpx}.is-unavailable{opacity:.56}.service-image{width:184rpx;height:184rpx;border-radius:17rpx;flex-shrink:0;background:#EEE}.service-info{display:flex;min-width:0;min-height:184rpx;flex:1;padding:4rpx 0 4rpx 24rpx;flex-direction:column;justify-content:space-between}.service-name{color:#292929;font-size:29rpx;font-weight:700}.discount-tag{display:block;width:fit-content;padding:6rpx 9rpx;border-radius:6rpx;background:#FFF1EC;color:#FF653D;font-size:21rpx}.address{display:flex;min-width:0;padding:7rpx 10rpx;border-radius:7rpx;background:#F1F2F3}.address image{width:27rpx;height:27rpx;margin-right:8rpx}.address text{min-width:0;flex:1;color:#7E8490;font-size:21rpx;line-height:31rpx}.list-state,.no-more{display:flex;height:150rpx;color:#989DA5;font-size:23rpx;align-items:center;justify-content:center}.list-state.compact{height:90rpx}.error-state{flex-direction:column}.retry-text,.load-error text{margin-top:18rpx;color:#F26A38}.loading-spinner{width:26rpx;height:26rpx;margin-right:12rpx;box-sizing:border-box;border:3rpx solid #FFD6C4;border-top-color:#FF6C2B;border-radius:50%;animation:loading 720ms linear infinite}@keyframes loading{to{transform:rotate(360deg)}}.bottom-space{height:calc(30rpx + env(safe-area-inset-bottom))}
|
</style>
|