fix(mobile): avoid status bar overlap on custom-nav pages

全局 navigationStyle 为 custom,页面需自行避让状态栏,但四处顶部内容仍被状态栏
压住:练习页标题与对话页标题、我的页顶部卡片、问答页头部。

避让不能走 CSS:env(safe-area-inset-top) 在 Android WebView 恒为 0(该值本是
iOS 刘海概念,Chromium 仅在配置 display-cutout 时才上报),uni-app 注入的
--status-bar-height 实测在 App 端同样取不到,两者都会静默回落成 0,造成"构建产物
里改动存在但运行时无效"。改由 utils/safe-area.ts 读
uni.getSystemInfoSync().statusBarHeight,各页面以内联样式绑定 padding-top。

profile 页同时把 min-height 从 100vh 改为 100%:叠加状态栏内边距后 100vh 会
溢出一屏高度产生多余滚动。

真机(vivo V2443A,720x1600,状态栏 72px)像素验证:修复前状态栏下缘 60-80px 带内
有 872 个文字像素,修复后四处均为 0。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-26 00:58:18 +08:00
co-authored by Claude Opus 5
parent 29bb10e1fa
commit 788a2abbe4
6 changed files with 52 additions and 6 deletions
+1
View File
@@ -24,3 +24,4 @@ mobile-uni/certificates/
mobile-uni/androidPrivacy.json mobile-uni/androidPrivacy.json
.tmp/ .tmp/
.worktrees/ .worktrees/
mobile-uni/.hbuilderx/
@@ -1,5 +1,5 @@
<template> <template>
<view class="ask-source-head glass-header"> <view class="ask-source-head glass-header" :style="{ paddingTop: safeTopPadding(6) }">
<view class="compact-header-row"> <view class="compact-header-row">
<!-- Left: Title & Status Badge --> <!-- Left: Title & Status Badge -->
<view class="brand-box"> <view class="brand-box">
@@ -40,6 +40,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { safeTopPadding } from '@/utils/safe-area';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
active: 'assistant' | 'web'; active: 'assistant' | 'web';
context?: string; context?: string;
@@ -67,7 +69,10 @@ const selectWeb = () => {
<style scoped> <style scoped>
.ask-source-head { .ask-source-head {
flex-shrink: 0; flex-shrink: 0;
padding: 6px 12px; /* 宿主页面根容器为 position: fixed; top: var(--window-top),custom 导航下解析为 0,
故白色头部需自行避让状态栏,避让后白底延伸至状态栏下方。 */
/* padding-top 由模板内联绑定(状态栏高度 + 6),见 utils/safe-area.ts */
padding: 6px 12px 6px;
background: #ffffff; background: #ffffff;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); border-bottom: 1px solid rgba(0, 0, 0, 0.05);
} }
+7 -2
View File
@@ -1,6 +1,6 @@
<template> <template>
<view class="page practice-page employee-page-shell"> <view class="page practice-page employee-page-shell">
<view class="practice-head"> <view class="practice-head" :style="{ paddingTop: safeTopPadding(12) }">
<text class="page-title">{{ practicePageTitle }}</text> <text class="page-title">{{ practicePageTitle }}</text>
<text class="page-subtitle">{{ practicePageSubtitle }}</text> <text class="page-subtitle">{{ practicePageSubtitle }}</text>
</view> </view>
@@ -276,7 +276,7 @@
</view> </view>
<view v-if="practiceView === 'active'" class="practice-chat"> <view v-if="practiceView === 'active'" class="practice-chat">
<view class="chat-head"> <view class="chat-head" :style="{ paddingTop: safeTopPadding(8) }">
<view class="chat-head-row"> <view class="chat-head-row">
<text class="chat-title">{{ activeScenarioName }}</text> <text class="chat-title">{{ activeScenarioName }}</text>
<text class="chat-context">{{ realtimeActive ? `实时 · ${realtimeStatusLabel}` : `第 ${Math.min(roundIndex + 1, activeRoundCount)}/${activeRoundCount} 轮` }}</text> <text class="chat-context">{{ realtimeActive ? `实时 · ${realtimeStatusLabel}` : `第 ${Math.min(roundIndex + 1, activeRoundCount)}/${activeRoundCount} 轮` }}</text>
@@ -531,6 +531,7 @@ import type { SpeechCapture, SpeechCaptureSession } from '@/services/speech-capt
import { createPageRequestScope, currentAccountKey, type RequestScopeTicket } from '@/services/request-scope'; import { createPageRequestScope, currentAccountKey, type RequestScopeTicket } from '@/services/request-scope';
import { createRealtimePracticeController, createRealtimeSession, fetchRealtimePersonas, invokeRealtimeTool, type RealtimePersonaOption } from '@/services/realtime-practice'; import { createRealtimePracticeController, createRealtimeSession, fetchRealtimePersonas, invokeRealtimeTool, type RealtimePersonaOption } from '@/services/realtime-practice';
import { stripInternalCodes } from '@/services/text'; import { stripInternalCodes } from '@/services/text';
import { safeTopPadding } from '@/utils/safe-area';
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished'; type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
type RecentPracticeItem = { type RecentPracticeItem = {
@@ -1849,6 +1850,8 @@ onUnload(() => {
.practice-head { .practice-head {
position: relative; position: relative;
margin: 0 -18px; margin: 0 -18px;
/* padding-top 由模板内联绑定(状态栏高度 + 12),见 utils/safe-area.ts;
此处 30px 仅为 H5 观感基线,App 端会被内联值覆盖。 */
padding: 30px 28px 24px; padding: 30px 28px 24px;
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%); background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
@@ -3357,6 +3360,8 @@ onUnload(() => {
.practice-chat .chat-head { .practice-chat .chat-head {
flex-shrink: 0; flex-shrink: 0;
/* .practice-chat 为 fixed 定位,不受页面内边距影响,头部需自行避让状态栏。 */
/* padding-top 由模板内联绑定(状态栏高度 + 8),见 utils/safe-area.ts */
padding: 8px 12px 8px; padding: 8px 12px 8px;
background: #ffffff; background: #ffffff;
border-bottom: 1px solid #ebedf0; border-bottom: 1px solid #ebedf0;
+5 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<view class="page profile-page employee-page-shell"> <view class="page profile-page employee-page-shell" :style="{ paddingTop: safeTopPadding(12) }">
<!-- Top Hero Header Card --> <!-- Top Hero Header Card -->
<view class="hero-identity-card"> <view class="hero-identity-card">
<view class="hero-brand-bar"> <view class="hero-brand-bar">
@@ -316,6 +316,7 @@ import {
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice'; import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
import { resetPageScroll } from '@/services/navigation'; import { resetPageScroll } from '@/services/navigation';
import { ensureEmployeePosition } from '@/services/position'; import { ensureEmployeePosition } from '@/services/position';
import { safeTopPadding } from '@/utils/safe-area';
import { import {
getTtsVoicePreference, getTtsVoicePreference,
getTtsDialectPreference, getTtsDialectPreference,
@@ -1074,9 +1075,11 @@ onShow(() => {
} }
.profile-page { .profile-page {
/* padding-top 由模板内联绑定(状态栏高度 + 12),见 utils/safe-area.ts */
padding: 12px 16px 36px; padding: 12px 16px 36px;
background: #f7f8fa; background: #f7f8fa;
min-height: 100vh; /* 改用 100% 而非 100vh:叠加状态栏内边距后 100vh 会溢出一屏高度产生多余滚动。 */
min-height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
+4
View File
@@ -36,6 +36,10 @@ page {
--employee-chip-radius: 18px; --employee-chip-radius: 18px;
--employee-card-shadow: 0 16px 40px rgba(230, 0, 18, 0.08); --employee-card-shadow: 0 16px 40px rgba(230, 0, 18, 0.08);
--app-bottom-nav-space: calc(82px + env(safe-area-inset-bottom)); --app-bottom-nav-space: calc(82px + env(safe-area-inset-bottom));
/* 顶部状态栏避让不走 CSS:env(safe-area-inset-top) 在 Android WebView 恒为 0,
uni-app 的 --status-bar-height 实测在 App 端同样取不到,两者都会静默回落成 0。
统一由 utils/safe-area.ts 读 uni.getSystemInfoSync().statusBarHeight,
各页面以内联样式绑定 padding-top。 */
} }
html[data-aihr-font-size='standard'] { html[data-aihr-font-size='standard'] {
+28
View File
@@ -0,0 +1,28 @@
/**
* 顶部状态栏高度。
*
* 全局 navigationStyle 为 custom,页面需自行避让状态栏。这里不能用 CSS 方案:
* - env(safe-area-inset-top) 在 Android WebView 恒为 0(该值本是 iOS 刘海概念,
* Chromium 仅在配置 display-cutout 时才上报);
* - uni-app 注入的 --status-bar-height 在 App 端实测同样取不到值。
* 两者都会静默回落成 0,造成"看起来改了但没生效"。故统一读 uni 的系统信息 API,
* 由各页面以内联样式绑定,行为跨端一致且可验证。
*/
let cached: number | null = null;
/** 状态栏高度(px);取不到时返回 0,H5 下即为 0。 */
export const statusBarHeight = (): number => {
if (cached !== null) return cached;
try {
const info = uni.getSystemInfoSync?.();
const height = typeof info?.statusBarHeight === 'number' ? info.statusBarHeight : 0;
cached = Number.isFinite(height) && height > 0 ? height : 0;
} catch {
cached = 0;
}
return cached;
};
/** 状态栏避让内边距:状态栏高度 + 额外留白。 */
export const safeTopPadding = (extra = 0) => `${statusBarHeight() + extra}px`;