feat:H5状态栏适配

This commit is contained in:
dzc committed 2026-07-06 13:16:03 +08:00
1 parent 3317d463a1
commit 8ad36b1d57
24 files changed
+271 -90

No files matched your search

+10 -4
View File
@@ -610,6 +610,12 @@ page {
@import url('./static/icon/self/iconfont.css');
// #ifdef H5
:root {
--native-status-bar-height: env(safe-area-inset-top, 0px);
--app-shell-status-bar-height: 0px;
--status-bar-height: var(--native-status-bar-height);
}
html,
body,
#app,
@@ -633,8 +639,8 @@ uni-page-wrapper {
display: flex;
flex-direction: column;
// #ifdef H5
height: calc(100dvh - 50px - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
top: $im-nav-bar-height;
height: calc(100dvh - 50px - $im-nav-bar-height - var(--status-bar-height, 0px)); // h5套壳时还要减去宿主app状态栏高度
top: calc($im-nav-bar-height + var(--status-bar-height, 0px));
overflow: hidden;
min-height: 0;
// #endif
@@ -657,8 +663,8 @@ uni-page-wrapper {
.page {
position: relative;
// #ifdef H5
height: calc(100vh - $im-nav-bar-height); // h5平台100vh是包含了底部高度,需要减去
top: $im-nav-bar-height;
height: calc(100dvh - $im-nav-bar-height - var(--status-bar-height, 0px)); // h5套壳时还要减去宿主app状态栏高度
top: calc($im-nav-bar-height + var(--status-bar-height, 0px));
overflow: hidden;
min-height: 0;
// #endif
+4 -2
View File
@@ -18,12 +18,14 @@ import * as recorder from './common/recorder-h5';
import ImageResize from 'quill-image-resize-mp';
import Quill from 'quill';
import { patchH5Navigation } from '@/sheep/router/h5-navigation';
import { initH5HostShell } from '@/sheep/platform/h5-shell';
// 以下组件用于兼容部分手机聊天边框无法输入的问题
window.Quill = Quill;
window.ImageResize = { default: ImageResize };
// 调试器
// import VConsole from 'vconsole';
// new VConsole();
import VConsole from 'vconsole';
new VConsole();
initH5HostShell();
patchH5Navigation();
// #endif
// #ifndef H5
+1 -1
View File
@@ -21,7 +21,7 @@
</template>
</wd-navbar>
<scroll-list v-if="userDetail?.member?.memberId" style="height: calc(100dvh - 88px)"
<scroll-list v-if="userDetail?.member?.memberId" style="height: calc(100dvh - 88px - var(--status-bar-height, 0px))"
:loadMoreStatus="loadMoreStatus" @refresh="refreshList" @loadMore="listLoadMore" @scroll="onScroll">
<view class="w-full h-600rpx relative">
<image class="w-full h-full absolute inset-0" :src="userDetail.member.memberAvatar" mode="aspectFill" />
+1 -1
View File
@@ -6,7 +6,7 @@
</template>
</wd-navbar>
<scroll-view scroll-y style="height: calc(100dvh - 88px)">
<scroll-view scroll-y style="height: calc(100dvh - 88px - var(--status-bar-height, 0px))">
<view class="mx-4 my-2 text-size-base">举报类型</view>
<view class="mx-4">
<wd-radio-group v-model="form.reportType" shape="dot" size="large">
+1 -1
View File
@@ -12,7 +12,7 @@
</template>
</wd-navbar>
<scroll-view scroll-y style="height: calc(100dvh - 88px)">
<scroll-view scroll-y style="height: calc(100dvh - 88px - var(--status-bar-height, 0px))">
<wd-textarea v-model="params.description" :maxlength="1000" clearable show-word-limit />
<wd-tabs v-model="activeTab">
<wd-tab title="发图文">
+1 -1
View File
@@ -7,7 +7,7 @@
</navigator>
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<view class="flex-col justify-center card-thin mx-20rpx px-30rpx py-30rpx mb-30rpx"
v-for="(v, i) in listData" :key="v.id">
+1 -1
View File
@@ -24,7 +24,7 @@
<!-- #endif -->
</wd-navbar>
<scroll-view style="height: calc(100dvh - 44px)" scroll-y>
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px))" scroll-y>
<view
class="mx-4 p-4 rounded-2 bg-gradient-to-b from-#111 to-#222 b-4 b-solid b-t-#D29E52/60 b-l-#D29E52/40 b-r-#D29E52/40 b-b-#D29E52/20">
<view class="text-color-theme-dark font-900 font-italic text-xl mb-1">VIP</view>
+1 -1
View File
@@ -8,7 +8,7 @@
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<view v-for="(item, index) in listData" :key="index" class="flex items-center justify-between px-4 py-2">
<view class="flex items-center text-base overflow-hidden">
+2 -2
View File
@@ -14,10 +14,10 @@
<swiper class="flex-1 overflow-hidden" :current="current" @change="(e) => (current = e.detail.current)"
:duration="200">
<swiper-item>
<scroll-list class="h-full" :loadMoreStatus="loadMoreStatus0" @refresh="refreshList0" @loadMore="listLoadMore0"
<scroll- list class="h-full" :loadMoreStatus="loadMoreStatus0" @refresh="refreshList0" @loadMore="listLoadMore0"
:show-scrollbar="false" :refresherEnabled="true">
<user-card v-for="user in listData0" :key="user.id" :data="user" @click="detail(user)" />
</scroll-list>
</scroll->
</swiper-item>
<swiper-item>
<scroll-list class="h-full" :loadMoreStatus="loadMoreStatus1" @refresh="refreshList1" @loadMore="listLoadMore1"
+1 -1
View File
@@ -15,7 +15,7 @@
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<moment-card v-for="item in listData" :data="item" :showChat="false" :showLike="false" />
</scroll-list>
+1 -1
View File
@@ -8,7 +8,7 @@
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<view class="flex items-center card-thin mx-20rpx px-30rpx py-30rpx mb-30rpx" v-for="(v, i) in listData"
:key="v.id">
+1 -1
View File
@@ -25,7 +25,7 @@
<!-- #endif -->
</wd-navbar>
<scroll-view style="height: calc(100dvh - 44px)" scroll-y>
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px))" scroll-y>
<view
class="mx-4 p-4 rounded-2 bg-gradient-to-b from-#111 to-#222 b-4 b-solid b-t-#D29E52/60 b-l-#D29E52/40 b-r-#D29E52/40 b-b-#D29E52/20">
<view class="text-color-theme-dark font-900 font-italic text-xl mb-1">VIP</view>
+1 -1
View File
@@ -10,7 +10,7 @@
<wd-tabs v-model="activeTab" swipeable>
<wd-tab title="我看过的">
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus_seenIt"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus_seenIt"
@refresh="refreshList_seenIt" @loadMore="listLoadMore_seenIt">
<view class="h-10rpx"></view>
<view-record-item v-if="listData_seenIt.length" v-for="user in listData_seenIt" :record="user"
+2 -2
View File
@@ -13,8 +13,8 @@
</view>
</template>
</wd-navbar>
<swiper :current="current" style="height: calc(100dvh - 44px - 50px)" @change="(e) => (current = e.detail.current)"
:duration="200">
<swiper :current="current" style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))"
@change="(e) => (current = e.detail.current)" :duration="200">
<swiper-item v-for="(item, index) in segmentedItems" :key="index">
<scroll-user :scene="item['search-action']" />
</swiper-item>
+1 -1
View File
@@ -10,7 +10,7 @@
<!-- #endif -->
</template>
</wd-navbar>
<scroll-view style="height: calc(100dvh - 44px);" scroll-y>
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px));" scroll-y>
<view style="background: red; height: var(--status-bar-height);"></view>
<!-- 用户信息 -->
<view class="mx-20rpx mt-30rpx bg-color-dark rounded-12rpx flex justify-between items-center px-6 py-4">
+1 -1
View File
@@ -7,7 +7,7 @@
</navigator>
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<GiftRecordItem v-for="(v, i) in listData" :key="v.id" :item="v" @on-refresh="refreshList" />
</scroll-list>
+1 -1
View File
@@ -12,7 +12,7 @@
<wd-tab title="消费记录" />
<wd-tab title="缘豆记录" />
</wd-tabs>
<view class="mt-15rpx mx-30rpx" style="height: calc(100dvh - 44px - 84rpx);">
<view class="mt-15rpx mx-30rpx" style="height: calc(100dvh - 44px - 84rpx - var(--status-bar-height, 0px));">
<scroll-list class="h-full" v-if="[0].includes(activeTab)" :loadMoreStatus="loadMoreStatus_withraw"
@refresh="refreshList_withraw" @loadMore="listLoadMore_withraw">
<WithrawRecordItem v-for="(v, i) in listData_withraw" :key="v.id" :item="v" />
+1 -1
View File
@@ -36,7 +36,7 @@
</view>
<view class="mt-30rpx mx-30rpx rounded-12rpx">
<view class="text-base">礼物明细</view>
<scroll-list class="mt-15rpx" style="height: calc(100dvh - 44px - 350rpx);" :loadMoreStatus="loadMoreStatus"
<scroll-list class="mt-15rpx" style="height: calc(100dvh - 44px - 350rpx - var(--status-bar-height, 0px));" :loadMoreStatus="loadMoreStatus"
@refresh="refreshList" @loadMore="listLoadMore">
<view class="flex-col card-thin px-16rpx py-12rpx mb-15rpx" v-for="(v, i) in listData" :key="v.id">
<text class="text-xs">{{ v.title }}</text>
+1 -1
View File
@@ -7,7 +7,7 @@
</navigator>
</template>
</wd-navbar>
<scroll-list style="height: calc(100dvh - 44px - 50px)" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
<scroll-list style="height: calc(100dvh - 44px - 50px - var(--status-bar-height, 0px))" :loadMoreStatus="loadMoreStatus" @refresh="refreshList"
@loadMore="listLoadMore">
<WithrawRecordItem v-for="(v, i) in listData" :key="v.id" :item="v" @on-refresh="refreshList" />
</scroll-list>
@@ -1,73 +1,60 @@
<template>
<scroll-view
scroll-y
refresher-enabled
enable-back-to-top
refresher-background="#222"
:refresher-triggered="refresherTriggered"
:refresher-threshold="100"
@refresherabort="onRefresherAbort"
@refresherrefresh="onRefresherRefresh"
@scrolltolower="onScrollToLower"
@scroll="scroll"
>
<scroll-view scroll-y refresher-enabled enable-back-to-top refresher-background="#222"
:refresher-triggered="refresherTriggered" :refresher-threshold="100" @refresherabort="onRefresherAbort"
@refresherrefresh="onRefresherRefresh" @scrolltolower="onScrollToLower" @scroll="scroll">
<slot />
<uni-load-more
:status="loadMoreStatus"
:content-text="loadMoreContent"
@clickLoadMore="onScrollToLower"
/>
<uni-load-more :status="loadMoreStatus" :content-text="loadMoreContent" @clickLoadMore="onScrollToLower" />
<view style="height: 100rpx"></view>
</scroll-view>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { reactive, ref } from 'vue';
const props = withDefaults(
defineProps<{
loadMoreStatus?: 'more' | 'loading' | 'noMore';
}>(),
{
loadMoreStatus: 'noMore',
},
);
const emit = defineEmits<{
(e: 'refresh', done: () => void): void;
(e: 'loadMore'): void;
(e: 'scroll', event: any): void;
}>();
const props = withDefaults(
defineProps<{
loadMoreStatus?: 'more' | 'loading' | 'noMore';
}>(),
{
loadMoreStatus: 'noMore',
},
);
const emit = defineEmits<{
(e: 'refresh', done: () => void): void;
(e: 'loadMore'): void;
(e: 'scroll', event: any): void;
}>();
const refresherTriggered = ref(false);
const refresherTriggered = ref(false);
const loadMoreContent = reactive({
contentdown: '正在加载...',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了',
});
const loadMoreContent = reactive({
contentdown: '正在加载...',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了',
});
// 下拉刷新中止
const onRefresherAbort = () => {
// 下拉刷新中止
const onRefresherAbort = () => {
refresherTriggered.value = false;
};
// 触发下拉刷新
const onRefresherRefresh = () => {
refresherTriggered.value = true;
emit('refresh', () => {
refresherTriggered.value = false;
};
});
};
// 触发下拉刷新
const onRefresherRefresh = () => {
refresherTriggered.value = true;
emit('refresh', () => {
refresherTriggered.value = false;
});
};
// 触发上拉加载
const onScrollToLower = () => {
if (props.loadMoreStatus === 'loading' || props.loadMoreStatus === 'noMore') {
return;
}
emit('loadMore');
};
// 触发上拉加载
const onScrollToLower = () => {
if (props.loadMoreStatus === 'loading' || props.loadMoreStatus === 'noMore') {
return;
}
emit('loadMore');
};
const scroll = (e: any) => {
emit('scroll', e);
};
const scroll = (e: any) => {
emit('scroll', e);
};
</script>
+175
View File
@@ -0,0 +1,175 @@
const H5_STATUS_BAR_STORAGE_KEY = '__friendship_h5_status_bar_height__';
const H5_STATUS_BAR_QUERY_KEYS = ['statusBarHeight', 'safeTop', 'safeAreaTop', 'appStatusBarHeight'];
const H5_MIN_STATUS_BAR_HEIGHT = 22;
function normalizeStatusBarHeight(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const normalizedValue = typeof value === 'string' ? value.replace(/px$/i, '').trim() : value;
const height = Number.parseFloat(normalizedValue);
if (!Number.isFinite(height)) {
return null;
}
return Math.max(0, Math.min(height, 200));
}
function parseMessageData(data) {
if (!data) {
return null;
}
if (typeof data === 'string') {
try {
return JSON.parse(data);
} catch (error) {
return null;
}
}
return data;
}
function readStatusBarHeightFromQuery() {
const searchParamsList = [];
searchParamsList.push(new URLSearchParams(window.location.search));
const hashQuery = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : '';
if (hashQuery) {
searchParamsList.push(new URLSearchParams(hashQuery));
}
for (const params of searchParamsList) {
for (const key of H5_STATUS_BAR_QUERY_KEYS) {
const height = normalizeStatusBarHeight(params.get(key));
if (height !== null) {
return height;
}
}
}
return null;
}
function readStatusBarHeightFromWindow() {
const candidates = [
window.__APP_STATUS_BAR_HEIGHT__,
window.__UNI_STATUS_BAR_HEIGHT__,
window.__APP_SHELL_CONFIG__?.statusBarHeight,
window.__UNI_WEBVIEW_CONFIG__?.statusBarHeight,
];
for (const candidate of candidates) {
const height = normalizeStatusBarHeight(candidate);
if (height !== null) {
return height;
}
}
return null;
}
function readStatusBarHeightFromStorage() {
try {
return normalizeStatusBarHeight(window.localStorage.getItem(H5_STATUS_BAR_STORAGE_KEY));
} catch (error) {
return null;
}
}
function readNativeStatusBarHeight() {
if (typeof window === 'undefined') {
return 0;
}
const rootStyle = window.getComputedStyle(document.documentElement);
return normalizeStatusBarHeight(rootStyle.getPropertyValue('--native-status-bar-height')) ?? 0;
}
function ensureMinShellStatusBarHeight(height) {
if (height === null || height <= 0) {
return 0;
}
return Math.max(height, H5_MIN_STATUS_BAR_HEIGHT);
}
export function getH5HostStatusBarHeight() {
if (typeof window === 'undefined') {
return 0;
}
return ensureMinShellStatusBarHeight(
readStatusBarHeightFromQuery() ??
readStatusBarHeightFromWindow() ??
readStatusBarHeightFromStorage() ??
0,
);
}
export function applyH5HostStatusBarHeight(height) {
if (typeof window === 'undefined') {
return 0;
}
const safeTop = ensureMinShellStatusBarHeight(normalizeStatusBarHeight(height));
const nativeSafeTop = readNativeStatusBarHeight();
const finalSafeTop = Math.max(nativeSafeTop, safeTop);
const safeTopValue = `${safeTop}px`;
const finalSafeTopValue = `${finalSafeTop}px`;
const rootStyle = document.documentElement.style;
const bodyStyle = document.body?.style;
rootStyle.setProperty('--app-shell-status-bar-height', safeTopValue);
rootStyle.setProperty('--status-bar-height', finalSafeTopValue);
bodyStyle?.setProperty('--app-shell-status-bar-height', safeTopValue);
bodyStyle?.setProperty('--status-bar-height', finalSafeTopValue);
window.__APP_STATUS_BAR_HEIGHT__ = safeTop;
try {
window.localStorage.setItem(H5_STATUS_BAR_STORAGE_KEY, String(safeTop));
} catch (error) {
// ignore
}
window.dispatchEvent(
new CustomEvent('app-shell-status-bar-change', {
detail: { height: finalSafeTop, appHeight: safeTop, nativeHeight: nativeSafeTop },
}),
);
return finalSafeTop;
}
function resolveMessageStatusBarHeight(message) {
const data = parseMessageData(message?.data);
if (!data) {
return null;
}
return normalizeStatusBarHeight(
data.statusBarHeight ??
data.safeTop ??
data.safeAreaTop ??
data.payload?.statusBarHeight ??
data.payload?.safeTop,
);
}
export function initH5HostShell() {
if (typeof window === 'undefined') {
return 0;
}
const safeTop = applyH5HostStatusBarHeight(getH5HostStatusBarHeight());
if (window.__FRIENDSHIP_H5_SHELL_READY__) {
return safeTop;
}
window.__FRIENDSHIP_H5_SHELL_READY__ = true;
window.setAppShellStatusBarHeight = (nextHeight) => applyH5HostStatusBarHeight(nextHeight);
window.addEventListener('message', (message) => {
const nextHeight = resolveMessageStatusBarHeight(message);
if (nextHeight !== null) {
applyH5HostStatusBarHeight(nextHeight);
}
});
window.addEventListener('load', () => {
applyH5HostStatusBarHeight(getH5HostStatusBarHeight());
});
return safeTop;
}
+7
View File
@@ -12,6 +12,7 @@
import { isEmpty } from 'lodash-es';
// #ifdef H5
import { isWxBrowser } from '@/sheep/helper/utils';
import { getH5HostStatusBarHeight } from './h5-shell';
// #endif
import wechat from './provider/wechat/index.js';
import apple from './provider/apple';
@@ -19,6 +20,12 @@ import share from './share';
import Pay from './pay';
const device = uni.getWindowInfo();
// #ifdef H5
const h5StatusBarHeight = getH5HostStatusBarHeight();
if (h5StatusBarHeight > 0) {
device.statusBarHeight = h5StatusBarHeight;
}
// #endif
const os = uni.getDeviceInfo().platform;
+1 -1
View File
@@ -6,7 +6,7 @@
<script setup>
import sheep from '@/sheep';
const statusBarHeight = sheep.$platform.device.statusBarHeight + 'px';
const statusBarHeight = `var(--status-bar-height, ${sheep.$platform.device.statusBarHeight}px)`;
</script>
<style lang="scss">
.uni-status-bar {
@@ -1,12 +1,14 @@
<template>
<view :style="{ height: addUnit(height) }">
<view :class="`wd-navbar ${customClass} ${fixed ? 'is-fixed' : ''} ${bordered ? 'is-border' : ''}`" :style="rootStyle">
<view :class="`wd-navbar ${customClass} ${fixed ? 'is-fixed' : ''} ${bordered ? 'is-border' : ''}`"
:style="rootStyle">
<view class="wd-navbar__content">
<view class="wd-navbar__capsule" v-if="$slots.capsule">
<slot name="capsule" />
</view>
<view :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft" v-else-if="!$slots.left">
<view :class="`wd-navbar__left ${leftDisabled ? 'is-disabled' : ''}`" @click="handleClickLeft"
v-else-if="!$slots.left">
<wd-icon v-if="leftArrow" name="arrow-left" custom-class="wd-navbar__arrow" />
<view v-if="leftText" class="wd-navbar__text">{{ leftText }}</view>
</view>
@@ -19,10 +21,12 @@
<slot name="title" />
<block v-if="!$slots.title && title">{{ title }}</block>
</view>
<view :class="`wd-navbar__right ${rightDisabled ? 'is-disabled' : ''}`" @click="handleClickRight" v-if="$slots.right || rightText">
<view :class="`wd-navbar__right ${rightDisabled ? 'is-disabled' : ''}`" @click="handleClickRight"
v-if="$slots.right || rightText">
<slot name="right" />
<view v-if="!$slots.right && rightText" class="wd-navbar__text" hover-class="wd-navbar__text--hover" :hover-stay-time="70">
<view v-if="!$slots.right && rightText" class="wd-navbar__text" hover-class="wd-navbar__text--hover"
:hover-stay-time="70">
{{ rightText }}
</view>
</view>
@@ -52,7 +56,7 @@ const emit = defineEmits(['click-left', 'click-right'])
const height = ref<number | ''>('') // 占位高度
const { statusBarHeight } = uni.getSystemInfoSync()
let { statusBarHeight } = uni.getSystemInfoSync()
watch(
[() => props.fixed, () => props.placeholder],
@@ -68,7 +72,7 @@ const rootStyle = computed(() => {
style['z-index'] = props.zIndex
}
if (props.safeAreaInsetTop) {
style['padding-top'] = addUnit(statusBarHeight || 0)
style['padding-top'] = `var(--status-bar-height, ${addUnit(statusBarHeight || 0)})`
}
return `${objToStyle(style)}${props.customStyle}`
})