diff --git a/src/pages/hotel/detail.vue b/src/pages/hotel/detail.vue index 85fb088..542447d 100644 --- a/src/pages/hotel/detail.vue +++ b/src/pages/hotel/detail.vue @@ -21,7 +21,7 @@ - + {{ hotel.hotelName }} @@ -87,8 +87,8 @@ - - + + 起价 ¥ @@ -112,6 +112,7 @@ const STAR_MAP: Record = { 1: "民宿", 2: "经济", 3: "舒适" const placeholderCover = "/static/page/blue-bg.png" const statusBarHeight = ref(20) +const safeBottomPx = ref(0) const scrollHeight = ref("100vh") const hotelId = ref("") const MAX_STAY_NIGHTS = 30 @@ -123,6 +124,13 @@ const hotel = ref(null) const products = ref([]) const rooms = ref([]) +/** 底部栏内容高度(上内边距 + 按钮 + 下内边距),不含安全区 */ +const BOTTOM_BAR_INNER_RPX = 16 + 84 + 16 +/** 无底栏时内容区额外留白 */ +const BODY_PAD_BASE_RPX = 24 +/** 有底栏时内容区额外缓冲,避免最后一张房型贴边 */ +const BODY_PAD_EXTRA_RPX = 24 + const nightCount = computed(() => { const n = dayjs(checkOut.value).diff(dayjs(checkIn.value), "day") return n > 0 ? n : 1 @@ -212,6 +220,26 @@ const minPrice = computed(() => { return nums.length ? Math.min(...nums) : null }) +const showBottomBar = computed(() => !!(hotel.value && roomCards.value.length)) + +const rpx2px = (rpx: number) => { + try { + return uni.upx2px(rpx) + } catch { + return Math.round(rpx / 2) + } +} + +/** 滚动区底部留白:底栏高度 + 安全区,避免 iOS Home 条 / Android 手势条遮住房型 */ +const bodyPadBottom = computed(() => { + if (showBottomBar.value) { + return `${rpx2px(BOTTOM_BAR_INNER_RPX + BODY_PAD_EXTRA_RPX) + safeBottomPx.value}px` + } + return `${rpx2px(BODY_PAD_BASE_RPX) + safeBottomPx.value}px` +}) + +const bottomBarPadBottom = computed(() => `${rpx2px(16) + safeBottomPx.value}px`) + const formatMd = (ymd: string) => dayjs(ymd).format("M月D日") const formatPrice = (v: any) => { const n = Number(v) @@ -226,9 +254,21 @@ const facilityIcon = (name: string) => { return "checkbox-circle-line" } +const readSafeBottom = (sys: any) => { + const inset = sys?.safeAreaInsets?.bottom + if (typeof inset === "number" && inset >= 0) return inset + const area = sys?.safeArea + if (area && typeof sys?.screenHeight === "number") { + const bottom = sys.screenHeight - (area.bottom || 0) + if (bottom > 0 && bottom < 80) return bottom + } + return 0 +} + const layout = () => { const sys = uni.getSystemInfoSync() statusBarHeight.value = sys.statusBarHeight || 20 + safeBottomPx.value = readSafeBottom(sys) scrollHeight.value = `${sys.windowHeight || 667}px` } @@ -255,7 +295,6 @@ const load = async () => { const book = (p: any) => { if (!p?.productCode) return - // 页面跳转 query 只带关键键;名称/地址/封面等进确认页后用详情接口补齐,避免整单信息挂在 URL 上 router.jump("/pages/hotel/confirm", { hotelId: hotelId.value, productCode: p.productCode, @@ -272,7 +311,6 @@ onLoad((opt: any) => { checkIn.value = opt?.checkIn || checkIn.value checkOut.value = opt?.checkOut || checkOut.value roomsCount.value = Number(opt?.rooms || 1) || 1 - // 连住最多 30 晚,避免超长区间带入确认页 const nights = dayjs(checkOut.value).diff(dayjs(checkIn.value), "day") if (nights > MAX_STAY_NIGHTS) { checkOut.value = dayjs(checkIn.value).add(MAX_STAY_NIGHTS, "day").format("YYYY-MM-DD") @@ -320,9 +358,12 @@ onLoad((opt: any) => { } .ht-body { margin-top: -36rpx; - padding: 0 24rpx 24rpx; + padding-left: 24rpx; + padding-right: 24rpx; + /* padding-bottom 由 bodyPadBottom 动态计算(底栏 + iOS/Android 安全区) */ position: relative; z-index: 2; + box-sizing: border-box; } .ht-card { background: #fff; @@ -492,11 +533,16 @@ onLoad((opt: any) => { right: 0; bottom: 0; z-index: 20; - padding: 16rpx 28rpx calc(16rpx + env(safe-area-inset-bottom)); + padding-top: 16rpx; + padding-left: 28rpx; + padding-right: 28rpx; + padding-bottom: calc(16rpx + constant(safe-area-inset-bottom)); + padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: #fff; display: flex; align-items: center; gap: 20rpx; + box-sizing: border-box; box-shadow: 0 -4rpx 20rpx rgba(15, 23, 42, 0.06); } .ht-bottom__price {