|
@@ -0,0 +1,113 @@
|
|
|
|
|
+import { Injectable } from '@angular/core';
|
|
|
|
|
+import { isNativeApp } from './api/platform-proxy.service';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 原生安全区修正(2026-11-15 真机实测发现)
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 问题
|
|
|
|
|
+ *
|
|
|
|
|
+ * 真机(小米 Android 16)上,会话列表页的**状态栏直接压在界面元素上**:
|
|
|
|
|
+ * 左上角菜单按钮遮住了时间,右上角「新建」按钮和电池/信号重叠。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 根因
|
|
|
|
|
+ *
|
|
|
|
|
+ * 页面本身是对的 —— 全项目 8 处顶栏/抽屉/登录页都用了 `var(--mob-safe-top)`,
|
|
|
|
|
+ * 而这个变量定义是 `env(safe-area-inset-top, 0px)`。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 但 **Capacitor WebView 里这个 env() 恒为 0**:
|
|
|
|
|
+ * · Android 15+(API 35+)默认强制 edge-to-edge,WebView 铺满到屏幕顶端
|
|
|
|
|
+ * · `safe-area-inset-*` 只在系统**显式告诉 WebView「你的内容与系统栏重叠」**时才有值
|
|
|
|
|
+ * · Capacitor 的 WebView 未被标记为 edge-to-edge 内容,于是系统认为「没重叠」→ 返回 0
|
|
|
|
|
+ *
|
|
|
|
|
+ * 结果:视觉上重叠了,CSS 却以为没重叠 → 顶栏顶到状态栏底下。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 做法
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用 `@capacitor/status-bar` 读**系统状态栏的真实高度**,写入 `--mob-safe-top`。
|
|
|
|
|
+ * 这样全项目所有 `var(--mob-safe-top)` 一次性修好,**不用改任何页面组件**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 为什么不用改 `viewport-fit=cover`
|
|
|
|
|
+ *
|
|
|
|
|
+ * 去掉它会让底部 safe-area 也失效(iPhone 的 Home 指示条、Android 手势条),
|
|
|
|
|
+ * 而底部 TabBar 已经正确地依赖 `--mob-safe-bottom`。**不能动全局 viewport 配置**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 为什么不直接用 Capacitor 的 `StatusBar.setOverlaysWebView(false)`
|
|
|
|
|
+ *
|
|
|
|
|
+ * 那会让 WebView **不铺到状态栏下方**(内容区从状态栏下沿开始),
|
|
|
|
|
+ * 视觉上是「状态栏一条空白」,而不是「页面背景延伸到状态栏下方」。
|
|
|
|
|
+ * 后者才是现代 App 的标准观感(微信/飞书都是内容延伸到透明状态栏下)。
|
|
|
|
|
+ */
|
|
|
|
|
+@Injectable({ providedIn: 'root' })
|
|
|
|
|
+export class SafeAreaService {
|
|
|
|
|
+ /** 已应用过(避免重复注册监听) */
|
|
|
|
|
+ private applied = false;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 校正顶部安全区。非原生环境直接返回,浏览器行为完全不变。
|
|
|
|
|
+ */
|
|
|
|
|
+ async apply(): Promise<void> {
|
|
|
|
|
+ if (!isNativeApp() || this.applied) return;
|
|
|
|
|
+ this.applied = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { StatusBar, Style } = await import('@capacitor/status-bar');
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 状态栏**浮在** WebView 之上(内容铺到它下面),
|
|
|
|
|
+ * 与页面「背景不断、内容让开」的设计一致。
|
|
|
|
|
+ */
|
|
|
|
|
+ await StatusBar.setOverlaysWebView({ overlay: true });
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 深色图标还是浅色图标,取当前主题。
|
|
|
|
|
+ * 亮色背景 → 深色图标(Style.Dark 在 Capacitor 里指的是「深色文字」);
|
|
|
|
|
+ * 暗色背景 → 浅色图标。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 主题切换时也要跟着改,否则暗色模式下状态栏黑字黑底看不见。
|
|
|
|
|
+ */
|
|
|
|
|
+ const applyStyle = () => {
|
|
|
|
|
+ const dark = document.documentElement.classList.contains('dark');
|
|
|
|
|
+ void StatusBar.setStyle({ style: dark ? Style.Light : Style.Dark });
|
|
|
|
|
+ };
|
|
|
|
|
+ applyStyle();
|
|
|
|
|
+
|
|
|
|
|
+ /* 监听 <html class="dark"> 变化,主题切换时同步状态栏图标色 */
|
|
|
|
|
+ new MutationObserver(applyStyle).observe(document.documentElement, {
|
|
|
|
|
+ attributes: true,
|
|
|
|
|
+ attributeFilter: ['class'],
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 读真实高度并写入 CSS 变量。
|
|
|
|
|
+ *
|
|
|
|
|
+ * `getInfo()` 返回的是**物理像素**(density 相关),
|
|
|
|
|
+ * 而 CSS 变量要的是逻辑像素 → 必须除以 devicePixelRatio。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 万一插件拿不到高度(某些 ROM),退回保守值 24px ——
|
|
|
|
|
+ * 宁可多留一点,也不要让状态栏压住按钮。
|
|
|
|
|
+ */
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 读真实高度并写入 CSS 变量。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 `height` 返回的**已经是 CSS 像素**,不要再除 devicePixelRatio!
|
|
|
|
|
+ * 实测(小米 Android 16,dpr 3.25):
|
|
|
|
|
+ * getInfo().height === 46 ← 就是 CSS px,与物理 152px 对应
|
|
|
|
|
+ * 误当物理像素再除 3.25 → 14px ← 顶栏只让开一点点,状态栏仍压住内容
|
|
|
|
|
+ * 这个坑我第一次就踩了,靠 CDP 读回 `--mob-safe-top` 才发现。
|
|
|
|
|
+ */
|
|
|
|
|
+ const info = await StatusBar.getInfo();
|
|
|
|
|
+ const cssPx = info.height && info.height > 0 ? info.height : 24;
|
|
|
|
|
+ this.setSafeTop(Math.max(0, Math.round(cssPx)));
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 插件不可用 / 权限问题:退回保守值,保证状态栏不会压住内容。
|
|
|
|
|
+ * 宁可多一点留白,也不要出现按钮被遮住这种硬伤。
|
|
|
|
|
+ */
|
|
|
|
|
+ this.setSafeTop(24);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 写入 `--mob-safe-top`(同时覆盖 root,全局生效) */
|
|
|
|
|
+ private setSafeTop(px: number): void {
|
|
|
|
|
+ document.documentElement.style.setProperty('--mob-safe-top', `${px}px`);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|