Docker Build and Push / build-image (push) Successful in 3m52s
根因: Tauri targetSdk 37 强制 edge-to-edge, MainActivity 不处理 IME Insets, 系统不再因键盘缩小窗口, 清单 adjustResize 失效, 键盘直接盖住 WebView。 - 新增 scripts/ci/android-softinput.sh: init 后注入 windowSoftInputMode=adjustResize, 并重写 MainActivity.kt 监听 IME Insets, 键盘可见时把高度加到内容容器底部 (API 30+), 让 WebView 真正变矮, 所有 bottom 弹层自动上移 - CI (android-build.yml) 与本地 build-apk.sh 在 init 之后、build 之前调用该脚本 (幂等) - 新增全局 useKeepInputVisible: 输入框聚焦后用 scroll-into-view-if-needed 滚入可视区, 覆盖页面滚动与 Popup 内部滚动 - 弹层补 max-height + 内部滚动, 防止键盘弹出后顶部被顶出屏幕 - calibrateSafeTop 在键盘弹出时保留旧值, 避免误判导致整页跳动
88 lines
4.0 KiB
React
88 lines
4.0 KiB
React
// 移动端 UI 框架: 全局样式 + 主题 (antd-mobile 5)
|
|
import 'antd-mobile/es/global'
|
|
|
|
import React from 'react'
|
|
import ReactDOM from 'react-dom/client'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import App from './App.jsx'
|
|
import { AuthProvider } from './auth/AuthContext.jsx'
|
|
import { initOta } from './api/ota.js'
|
|
import { startIfinanceAutoRefresh } from './lib/ifinance.js'
|
|
import './styles/globals.css'
|
|
|
|
// OTA 启动: 仅 Tauri 内生效
|
|
// - notifyReady: 确认当前资源可用, 否则下次启动自动回滚 (hotswap 插件行为)
|
|
// - 异步执行, 不阻塞 React 挂载
|
|
initOta()
|
|
|
|
// ifinance 索引每日自动更新 (启动 4s 后检查一次, 之后每 30 分钟看是否已过 24h)
|
|
startIfinanceAutoRefresh()
|
|
|
|
// ==================== 顶部状态栏安全区校准 ====================
|
|
// 背景: Android WebView (Tauri) 下 env(safe-area-inset-top) 常返回 0,
|
|
// 而 antd-mobile 的 <SafeArea> 只是个依赖该变量的空 div, 于是内容会顶到状态栏下.
|
|
// 这里用实测值覆盖 --safe-top, 保证不同机型/不同原生配置下都能正确让位.
|
|
function calibrateSafeTop() {
|
|
const root = document.documentElement
|
|
|
|
// 校准只对 Android 有意义 (状态栏/刘海让位)。桌面 (Windows WebView2 / 桌面浏览器)
|
|
// 没有状态栏: env() 本来就是 0; 而「screen.height - visualViewport.height」在桌面
|
|
// 竖屏窗口下会把标题栏+边框高度 (~99px) 误判成状态栏, 让标题栏下多出一条大空隙
|
|
// (2026-10-26 Windows 外壳实测)。非 Android 一律 0。
|
|
const isAndroid = /android/i.test(navigator.userAgent)
|
|
if (!isAndroid) {
|
|
root.style.setProperty('--safe-top', '0px')
|
|
return
|
|
}
|
|
|
|
// 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
|
|
// 软键盘弹出会让 visualViewport 高度骤减 (diff 远大于状态栏高度)。
|
|
// 此时必须保留上一次算出的值, 否则会被当成"没有状态栏"而改成 24px, 整页跳动。
|
|
if (isPortrait && diff >= 120 && root.style.getPropertyValue('--safe-top')) return
|
|
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
|
|
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>
|
|
<AuthProvider>
|
|
<App />
|
|
</AuthProvider>
|
|
</BrowserRouter>
|
|
</React.StrictMode>,
|
|
)
|