| | |
| | | </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> |
| | | <swiper v-if="scenes.length > 0" class="hero-swiper" :current="activeIndex < 0 ? 0 : activeIndex" :circular="scenes.length > 1" :duration="240" @change="handleHeroChange"> |
| | | <swiper-item v-for="(item, index) in scenes" :key="item.id"> |
| | | <image class="hero-image" :src="item.image" mode="aspectFill" @error="replaceSceneImage(index)"></image> |
| | | </swiper-item> |
| | | </swiper> |
| | | <image v-else 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 :show-scrollbar="false" |
| | |
| | | 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="service-info"><text class="service-name">{{item.name}}</text><view v-if="item.available" class="discount-tags"><text v-for="tag in item.discountTags" :key="tag" :class="tag == '券' ? 'coupon-tag' : 'discount-tag'">{{tag}}</text></view><text v-else class="discount-tag">已下架</text> |
| | | <view v-if="item.merchantType != 'apartment'" class="address"> |
| | | <image src="/static/benefits-assets/map-pin.svg"></image><text>{{item.address}}</text> |
| | | </view> |
| | |
| | | import { getStayTopic, getStayTopicMerchants } from '../../services/portal.uts' |
| | | import { resolveApiUrl } from '../../services/http.uts' |
| | | import { safeBack } from '../../utils/navigation.uts' |
| | | import { resolveTalentId } from '../../utils/talent-id.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 = '' |
| | | 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) |
| | | const talentId = ref('') |
| | | |
| | | 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' } |
| | | talentId.value = resolveTalentId() |
| | | loadStayTopic(); loadInitialServices() |
| | | }) |
| | | const readText = (primary : any, fallback : any) : string => { |
| | |
| | | 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 handleHeroChange = (event : any) => { const current = event != null && event.detail != null && event.detail.current != null ? Number(event.detail.current) : -1; if (current < 0) return; selectScene(current) } |
| | | 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) => { |
| | |
| | | loadMoreError.value = true |
| | | return |
| | | } |
| | | getStayTopicMerchants(activeTab.value, currentPage.value + 1, 4, (data : any) => { |
| | | getStayTopicMerchants(activeTab.value, currentPage.value + 1, 4, talentId.value, (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() : [] |
| | |
| | | const merchantType = readText(item.merchantType, item['merchant_type']) |
| | | const merchantName = readText(item.merchantName, item['merchant_name']) |
| | | const discountText = readText(item.discountText, item['discount_text']) |
| | | const hasConfiguredTags = item.benefitTags instanceof Array |
| | | const configuredTags: any[] = hasConfiguredTags ? (item.benefitTags as any[]) : [] |
| | | const displayTags: string[] = [] |
| | | for (let tagIndex = 0; tagIndex < configuredTags.length; tagIndex++) { |
| | | const tag = String(configuredTags[tagIndex]).trim() |
| | | if (tag != '' && tag != '人才专享' && displayTags.indexOf(tag) < 0) displayTags.push(tag) |
| | | } |
| | | const hasCoupon = item.couponAvailable === true || Number(item.couponAvailable) == 1 |
| | | if (!hasConfiguredTags) { |
| | | if (hasCoupon) displayTags.push('券') |
| | | if (discountText != '' && discountText != '人才专享') displayTags.push(discountText) |
| | | } |
| | | next.push({ |
| | | id: '' + item.id, |
| | | merchantType: merchantType != '' ? merchantType : activeTab.value, |
| | | name: merchantName != '' ? merchantName : '\u4f4f\u5bbf\u670d\u52a1', |
| | | discount: discountText != '' ? discountText : '\u4eba\u624d\u4e13\u4eab', |
| | | discountTags: displayTags, |
| | | address: readText(item.address, item['merchant_address']), |
| | | image: coverUrl != '' ? resolveApiUrl(coverUrl) : fallbackImage, |
| | | available: readAvailable(item) |
| | |
| | | width: 100%; |
| | | height: 590rpx; |
| | | } |
| | | /* 主图轮播:与下方缩略图带同高同位,盖在原来的静态主图上。 */ |
| | | .hero-swiper { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | width: 100%; |
| | | height: 590rpx; |
| | | } |
| | | .hero-shade { |
| | | position: absolute; |
| | | left: 0; |
| | |
| | | top: 245rpx; |
| | | max-width: 650rpx; |
| | | color: #FFFFFF; |
| | | font-family: 'StayCalligraphy', 'STXingkai', 'KaiTi', cursive; |
| | | font-family: 'FoodCalligraphyFull', 'StayCalligraphy', 'STXingkai', 'KaiTi', cursive; |
| | | font-size: 48rpx; |
| | | font-weight: normal; |
| | | font-style: normal; |
| | |
| | | box-sizing: border-box; |
| | | padding: 3rpx 8rpx; |
| | | border-radius: 5rpx; |
| | | background-color: #FFF0EB; |
| | | color: #FF6239; |
| | | background-color: #FFF5E7; |
| | | color: #FF7C2B; |
| | | font-size: 21rpx; |
| | | line-height: 29rpx; |
| | | } |
| | | .coupon-tag { |
| | | display: block; |
| | | width: fit-content; |
| | | box-sizing: border-box; |
| | | padding: 3rpx 8rpx; |
| | | border-radius: 5rpx; |
| | | background-color: #FFF0EC; |
| | | color: #FF5D37; |
| | | font-size: 21rpx; |
| | | line-height: 29rpx; |
| | | } |
| | | .discount-tags { display:flex; flex-direction:row; flex-wrap:wrap; gap:8rpx; } |
| | | .address { |
| | | display: flex; |
| | | max-width: 100%; |