现象: 直装 APK 时头部正常让开状态栏; 经 OTA 升级到 V0.2 前端后, 页面从屏幕最顶部开始, 与电量/信号栏重叠. 根因: 顶部让位一直只依赖 antd-mobile 的 <SafeArea position="top" />, 它只是一个空 div, 高度完全来自 env(safe-area-inset-top). 而 Android WebView (Tauri) 下该变量常返回 0. 旧版每页顶部都有 45px 的 antd-mobile <NavBar>, 视觉上兜住了状态栏, 掩盖了 inset 失效; V0.2 把 NavBar 换成 23px 的 PageHeader (「我」页甚至无头部), 兜底消失后问题暴露. 与 OTA 机制本身无关. 修复: - MobileShell: <SafeArea> 换成 .app-safe-top, 高度由 --safe-top 控制 - main.jsx: 新增 calibrateSafeTop(), 用临时元素测 env(safe-area-inset-top), 并与 screen.height - visualViewport.height 交叉校验, 取较大值写入 --safe-top; 两者都不可用时保底 24px; 监听旋转/视口变化重算 - globals.css: --safe-top 保留 env() 表达式, 新增 .app-safe-top 样式 这样无论 WebView 是否上报 inset, 顶部都能正确让位.
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Outlet, useLocation } from 'react-router-dom'
|
import { Outlet, useLocation } from 'react-router-dom'
|
||||||
import { SafeArea } from 'antd-mobile'
|
|
||||||
import BottomNav from '../components/BottomNav.jsx'
|
import BottomNav from '../components/BottomNav.jsx'
|
||||||
|
|
||||||
// 移动端应用外壳
|
// 移动端应用外壳
|
||||||
// - 顶部 SafeArea: 让出手机状态栏高度
|
// - 顶部安全区: 用 --safe-top 撑开 (由 main.jsx 的 calibrateSafeTop 实测校准)
|
||||||
// - 中间 <main>: flex:1 可滚动主区
|
// - 中间 <main>: flex:1 可滚动主区
|
||||||
// - 底部 TabBar: antd-mobile TabBar 自带底部安全区
|
// - 底部 TabBar: antd-mobile TabBar 自带底部安全区
|
||||||
// 解决: 状态栏遮挡/底部 TabBar 遮挡/左右贴边
|
//
|
||||||
|
// 注意: 这里不再用 antd-mobile 的 <SafeArea position="top" />.
|
||||||
|
// 它只是一个空 div, 高度完全依赖 env(safe-area-inset-top); 而 Android WebView
|
||||||
|
// (Tauri) 下该变量常为 0, 会导致内容顶到状态栏底下 —— V0.2 把 45px 的 NavBar
|
||||||
|
// 换成 23px 的 PageHeader 后这个问题就暴露了. 改用可控高度的 div + JS 校准.
|
||||||
export default function MobileShell() {
|
export default function MobileShell() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
// pathname 变化时给页面容器加动画 class (仅播放入场动画, 不重挂载子树,
|
// pathname 变化时给页面容器加动画 class (仅播放入场动画, 不重挂载子树,
|
||||||
@@ -21,7 +24,7 @@ export default function MobileShell() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-root">
|
<div className="app-root">
|
||||||
<SafeArea position="top" />
|
<div className="app-safe-top" aria-hidden="true" />
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
<div className={'page-transition' + (animating ? ' active' : '')}>
|
<div className={'page-transition' + (animating ? ' active' : '')}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -30,4 +33,4 @@ export default function MobileShell() {
|
|||||||
<BottomNav />
|
<BottomNav />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,51 @@ import './styles/globals.css'
|
|||||||
// - 异步执行, 不阻塞 React 挂载
|
// - 异步执行, 不阻塞 React 挂载
|
||||||
initOta()
|
initOta()
|
||||||
|
|
||||||
|
// ==================== 顶部状态栏安全区校准 ====================
|
||||||
|
// 背景: Android WebView (Tauri) 下 env(safe-area-inset-top) 常返回 0,
|
||||||
|
// 而 antd-mobile 的 <SafeArea> 只是个依赖该变量的空 div, 于是内容会顶到状态栏下.
|
||||||
|
// 这里用实测值覆盖 --safe-top, 保证不同机型/不同原生配置下都能正确让位.
|
||||||
|
function calibrateSafeTop() {
|
||||||
|
const root = document.documentElement
|
||||||
|
|
||||||
|
// env() 的真实值必须用一个临时元素测量, 不能直接读 --safe-top ——
|
||||||
|
// 该变量是 max(env(...), 0px) 这样的表达式, parseFloat 会得到 NaN.
|
||||||
|
const probe = document.createElement('div')
|
||||||
|
probe.style.cssText =
|
||||||
|
'position:absolute;left:-9999px;top:0;width:1px;height:0;' +
|
||||||
|
'padding-top:env(safe-area-inset-top, 0px);visibility:hidden;pointer-events:none;'
|
||||||
|
document.body.appendChild(probe)
|
||||||
|
const envTop = parseFloat(getComputedStyle(probe).paddingTop) || 0
|
||||||
|
document.body.removeChild(probe)
|
||||||
|
|
||||||
|
// 屏幕顶部与 WebView 可视区顶部的差值 = 状态栏(及刘海)高度.
|
||||||
|
// 优先用 visualViewport (能反映真实可视区域), 回退 innerHeight.
|
||||||
|
const screenH = window.screen?.height || 0
|
||||||
|
const viewH = window.visualViewport?.height || window.innerHeight || 0
|
||||||
|
const screenW = window.screen?.width || 0
|
||||||
|
const viewW = window.visualViewport?.width || window.innerWidth || 0
|
||||||
|
// 只在"纵向且高度差合理"时采用, 避免横屏/桌面窗口误判
|
||||||
|
const isPortrait = viewW <= viewH
|
||||||
|
const diff = screenH - viewH
|
||||||
|
const measured = isPortrait && diff > 0 && diff < 120 ? diff : 0
|
||||||
|
|
||||||
|
// 取 env() 与实测值的较大者; 两者都失效时给 24px 保底
|
||||||
|
// (多数 Android 状态栏在 24~28px, 宁可多一点也不能压字)
|
||||||
|
let top = Math.max(envTop, measured)
|
||||||
|
if (top < 4) top = 24 // env 与实测都没拿到 → 用保守兜底, 保证不遮挡
|
||||||
|
root.style.setProperty('--safe-top', top + 'px')
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM 就绪后再校准 (需要往 body 插探测元素); 同步先跑一次保证首屏不闪
|
||||||
|
if (document.body) {
|
||||||
|
calibrateSafeTop()
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', calibrateSafeTop, { once: true })
|
||||||
|
}
|
||||||
|
// 旋转屏幕 / 视口变化时重算
|
||||||
|
window.addEventListener('orientationchange', () => setTimeout(calibrateSafeTop, 120))
|
||||||
|
window.visualViewport?.addEventListener('resize', calibrateSafeTop)
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
+13
-1
@@ -10,7 +10,11 @@
|
|||||||
--color-primary: #1677ff;
|
--color-primary: #1677ff;
|
||||||
--color-accent: #1677ff;
|
--color-accent: #1677ff;
|
||||||
--color-danger: #ff3141;
|
--color-danger: #ff3141;
|
||||||
--safe-top: env(safe-area-inset-top, 0px);
|
/* 顶部状态栏让位高度.
|
||||||
|
Android WebView (Tauri) 下 env(safe-area-inset-top) 常常返回 0, 仅靠它会让内容
|
||||||
|
顶到状态栏底下 (V0.2 移除了 45px 的 NavBar 后暴露). 这里给一个保守兜底值,
|
||||||
|
再由 main.jsx 的 calibrateSafeTop() 用实测高度覆盖 --safe-top, 双保险. */
|
||||||
|
--safe-top: max(env(safe-area-inset-top, 0px), 0px);
|
||||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
--safe-left: env(safe-area-inset-left, 0px);
|
--safe-left: env(safe-area-inset-left, 0px);
|
||||||
--safe-right: env(safe-area-inset-right, 0px);
|
--safe-right: env(safe-area-inset-right, 0px);
|
||||||
@@ -49,6 +53,14 @@ input, textarea, select { font: inherit; }
|
|||||||
padding-right: var(--safe-right);
|
padding-right: var(--safe-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 顶部状态栏安全区占位: 高度由 --safe-top 决定 (main.jsx 实测校准后覆盖) */
|
||||||
|
.app-safe-top {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: var(--safe-top);
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user