修复 OTA 后顶部内容顶到状态栏 (未让出电量/信号栏)
Docker Build and Push / build-image (push) Successful in 4m2s

现象: 直装 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:
jif2.zhang
2026-10-24 08:05:15 +08:00
parent 2f55865092
commit 2ea0da758f
3 changed files with 66 additions and 6 deletions
+45
View File
@@ -14,6 +14,51 @@ import './styles/globals.css'
// - 异步执行, 不阻塞 React 挂载
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(
<React.StrictMode>
<BrowserRouter>