From e52934f6527027bd6008c2ade61c3083159efd4a Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期五, 09 十月 2026 18:14:28 +0800
Subject: [PATCH] 更新....
---
rencaika/pages/stay-benefits/stay-benefits.uvue | 528 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 509 insertions(+), 19 deletions(-)
diff --git a/rencaika/pages/stay-benefits/stay-benefits.uvue b/rencaika/pages/stay-benefits/stay-benefits.uvue
index aab382a..5ea6c0a 100644
--- a/rencaika/pages/stay-benefits/stay-benefits.uvue
+++ b/rencaika/pages/stay-benefits/stay-benefits.uvue
@@ -1,9 +1,78 @@
<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>
+ <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" hover-class="back-button-active" @tap="goBack">
+ <image src="/static/food-assets/arrow-left-white.svg" mode="aspectFit"></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">
+ <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"
+ 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><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>
+ </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>
@@ -13,41 +82,462 @@
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 = '/static/stay-assets/hotel-vienna.jpg'
+ 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) => {
+ 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 => {
+ if (primary != null && primary != '') return primary as string
+ if (fallback != null && fallback != '') return fallback as string
+ return ''
+ }
+ const readAvailable = (item : any) : boolean => {
+ if (item.available != null) return item.available as boolean
+ if (item.status != null) return Number(item.status) == 1
+ return true
+ }
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 = '' })
+ getStayTopic((data : any) => {
+ topicDescription.value = data != null ? readText(data.description, data.intro) : ''
+ 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]
+ const imageUrl = readText(item.imageUrl, item['image_url'])
+ if (imageUrl == '') continue
+ next.push({
+ id: '' + item.id,
+ name: readText(item.name, item['item_name']),
+ image: resolveApiUrl(imageUrl)
+ })
+ }
+ 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 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) => {
+ 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 })
+ 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, 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() : []
+ for (let index = 0; index < records.length; index++) {
+ const item = records[index]
+ let coverUrl = readText(item.coverUrl, item['cover_url'])
+ if (coverUrl == '') coverUrl = readText(item.logoUrl, item['logo_url'])
+ 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)
+ })
+ }
+ 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) }) }
+ 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))}
+ @font-face {
+ font-family: 'StayCalligraphy';
+ src: url('/static/fonts/food-calligraphy-subset.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+ }
+
+ .page,
+ .page-scroll {
+ width: 100%;
+ height: 100vh;
+ background-color: #FFFFFF;
+ font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+ }
+
+ .custom-nav {
+ position: fixed;
+ z-index: 20;
+ left: 0;
+ right: 0;
+ top: 0;
+ padding-bottom: 10rpx;
+ }
+
+ .nav-bg {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ background-color: #F26A38;
+ transition: opacity 160ms ease-out;
+ }
+
+ .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: 20rpx;
+ padding-right: 10px;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .back-button {
+ display: flex;
+ width: 54rpx;
+ height: 54rpx;
+ position: relative;
+ z-index: 3;
+ border-radius: 50%;
+ background-color: rgba(27, 38, 62, .34);
+ align-items: center;
+ justify-content: center;
+ }
+ .custom-nav.is-solid .back-button { background-color: transparent; }
+ .back-button-active { opacity: .62; }
+ .back-button image { width: 36rpx; height: 36rpx; pointer-events: none; }
+
+ .nav-title {
+ position: absolute;
+ z-index: 1;
+ left: 0;
+ right: 0;
+ color: #FFFFFF;
+ font-size: 31rpx;
+ font-weight: 500;
+ line-height: 42rpx;
+ text-align: center;
+ text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, .32);
+ }
+ .capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
+
+ .hero {
+ height: 590rpx;
+ position: relative;
+ overflow: visible;
+ background-color: #1D315D;
+ }
+ .hero-image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 590rpx;
+ }
+ /* 主图轮播:与下方缩略图带同高同位,盖在原来的静态主图上。 */
+ .hero-swiper {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 590rpx;
+ }
+ .hero-shade {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ height: 300rpx;
+ background-image: linear-gradient(180deg, rgba(10, 22, 51, .55), rgba(10, 22, 51, 0));
+ }
+ .scene-name {
+ position: absolute;
+ z-index: 2;
+ left: 38rpx;
+ top: 245rpx;
+ max-width: 650rpx;
+ color: #FFFFFF;
+ font-family: 'FoodCalligraphyFull', 'StayCalligraphy', 'STXingkai', 'KaiTi', cursive;
+ font-size: 48rpx;
+ font-weight: normal;
+ font-style: normal;
+ line-height: 66rpx;
+ letter-spacing: 1rpx;
+ text-shadow: 0 3rpx 10rpx rgba(0, 0, 0, .55);
+ }
+
+ .no-gallery {
+ position: absolute;
+ z-index: 2;
+ top: 390rpx;
+ left: 0;
+ right: 0;
+ color: rgba(255,255,255,.9);
+ font-size: 23rpx;
+ text-align: center;
+ }
+
+ .scene-scroll {
+ position: absolute;
+ z-index: 4;
+ left: 0;
+ right: 0;
+ top: 385rpx;
+ height: 132rpx;
+ white-space: nowrap;
+ }
+ .scene-row {
+ display: flex;
+ height: 132rpx;
+ flex-direction: row;
+ align-items: center;
+ }
+ .scene-pad { width: 322rpx; height: 1rpx; flex-shrink: 0; }
+ .scene-item {
+ display: flex;
+ width: 106rpx;
+ height: 106rpx;
+ margin-right: 31rpx;
+ box-sizing: border-box;
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+ border: 3rpx solid rgba(255,255,255,.92);
+ border-radius: 8rpx;
+ flex-shrink: 0;
+ transition: transform 160ms ease-out;
+ }
+ .scene-item image { width: 100%; height: 100%; }
+ .scene-item.active {
+ z-index: 3;
+ border-width: 4rpx;
+ border-color: #FFD329;
+ transform: scale(1.13);
+ box-shadow: 0 8rpx 18rpx rgba(0,0,0,.28);
+ }
+
+ .topic-summary {
+ margin-top: -65rpx;
+ position: relative;
+ z-index: 5;
+ }
+ .intro-card {
+ display: flex;
+ min-height: 110rpx;
+ margin: 0 32rpx;
+ box-sizing: border-box;
+ padding: 18rpx 24rpx;
+ border-radius: 16rpx;
+ background-image: linear-gradient(90deg, #FF5425 0%, #FF861D 100%);
+ box-shadow: 0 9rpx 22rpx rgba(255, 104, 28, .26);
+ align-items: center;
+ }
+ .intro-card text {
+ color: #FFFFFF;
+ font-size: 23rpx;
+ line-height: 34rpx;
+ }
+
+ .service-section { padding: 20rpx 32rpx 0; }
+ .service-tabs {
+ display: flex;
+ height: 70rpx;
+ flex-direction: row;
+ align-items: flex-start;
+ }
+ .service-tabs > view {
+ display: flex;
+ margin-right: 44rpx;
+ flex-direction: column;
+ align-items: center;
+ }
+ .service-tabs text {
+ color: #8A8D92;
+ font-size: 31rpx;
+ font-weight: 600;
+ line-height: 42rpx;
+ }
+ .service-tabs .active text { color: #303238; font-weight: 700; }
+ .service-tabs view view {
+ width: 0;
+ height: 5rpx;
+ margin-top: 7rpx;
+ border-radius: 4rpx;
+ background-image: linear-gradient(90deg, #FF4D36, #FFBF3A);
+ transition: width 160ms ease-out;
+ }
+ .service-tabs .active view { width: 48rpx; }
+
+ .service-item {
+ display: flex;
+ min-height: 156rpx;
+ margin-bottom: 18rpx;
+ flex-direction: row;
+ align-items: flex-start;
+ }
+ .is-unavailable { opacity: .56; }
+ .service-image {
+ width: 136rpx;
+ height: 136rpx;
+ border-radius: 14rpx;
+ flex-shrink: 0;
+ background-color: #EEF0F2;
+ }
+ .service-info {
+ display: flex;
+ min-width: 0;
+ min-height: 136rpx;
+ box-sizing: border-box;
+ padding: 0 0 0 20rpx;
+ flex: 1;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ }
+ .service-name {
+ display: block;
+ width: 100%;
+ color: #292B30;
+ font-size: 29rpx;
+ font-weight: 700;
+ line-height: 40rpx;
+ }
+ .discount-tag {
+ display: block;
+ width: fit-content;
+ box-sizing: border-box;
+ padding: 3rpx 8rpx;
+ border-radius: 5rpx;
+ 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%;
+ box-sizing: border-box;
+ padding: 3rpx 7rpx;
+ border-radius: 5rpx;
+ background-color: #F3F4F5;
+ flex-direction: row;
+ align-items: center;
+ }
+ .address image { width: 21rpx; height: 21rpx; margin-right: 5rpx; flex-shrink: 0; }
+ .address text {
+ min-width: 0;
+ color: #7D838E;
+ font-size: 21rpx;
+ line-height: 31rpx;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .list-state,
+ .no-more {
+ display: flex;
+ height: 132rpx;
+ color: #989DA5;
+ font-size: 23rpx;
+ align-items: center;
+ justify-content: center;
+ }
+ .list-state.compact { height: 82rpx; }
+ .error-state { flex-direction: column; }
+ .retry-text,
+ .load-error text { margin-top: 16rpx; 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>
--
Gitblit v1.7.1