Files
MailUI4Agents/client/electron/src/main.tsx
JianFeeeee 8a9bc58433 feat(webui): 横竖屏自适应(紧凑双栏)+ 旋屏重算高度
用户:「为什么窄屏的横屏和竖屏没有自适应」。

## 根因

断点是**纯宽度**的:`(max-width: 1023px)`。手机竖屏 390 与横屏 844 **都落在同一档**
⇒ 布局一个像素都不变,横屏多出来的 ~450px 全浪费在空白上。看起来就是"没自适应"。

## 改动

- 新增 `useCompactTwoPane()`:`(min-width: 700px) and (max-height: 560px)`
  —— 仍是紧凑外壳(悬浮底部导航),但内容区从**覆盖式**改成**列表 + 详情并排**。
  用高度而不是 `landscape` 判:桌面窗口压扁、分屏、软键盘弹起都会命中,
  而它们要的是同一件事(别浪费横向空间)。
- App:`compactTwoPane && hasList` 时走并排分支,其余情况仍是 NarrowStack 覆盖式。
- `--app-height` 增加 `orientationchange` 监听并延后一帧重算(iOS 上部分版本的
  resize 不跟着方向变化走;立刻读会拿到旧高度)。

## 判据

`test/manual/rotate-verify.mjs` 8 条,含**可逆性**与前置对照:
竖屏覆盖式 → 横屏并排(真的换了 class)→ 旋回竖屏恢复覆盖式;
三态的 `--app-height` 分别为 844 / 390 / 844(不是旧值);横屏无横向溢出、
底部导航仍完整可见且浮着(left=20)。实测 **8/8**。

同一轮还补上了 `test/manual/calendar-swipe-verify.mjs`(日历滑动,4 条)。
2026-09-14 11:13:24 +08:00

68 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { initTheme } from './stores/themeStore';
import { initBackground } from './stores/backgroundStore';
import './index.css';
// 背景开启时要变半透明的浅色表面类(由 scripts/gen-background-takeover.mjs 生成)
import './background-takeover.generated.css';;
// 必须在 render 之前:晚一步就会让深色偏好的用户看到一帧白色闪屏。
// index.html 里还有一段更早的内联脚本处理「JS bundle 到达前」那段空窗,
// 这里做的是把 store 状态与 DOM 对齐并订阅系统主题变化。
initTheme();
// 自定义背景同样要在 render 之前套用(读 localStorage + 写 CSS 变量)。
// 它只是装饰层,晚一帧不会像主题那样闪出刺眼白底,但提前套用能避免
// 「先看到纯色底、再变成背景」的抽动。
initBackground();
/**
* 用 Visual Viewport 驱动应用高度。
*
* 手机软键盘通常只缩小「可见视口」,不缩小传统 100%/100vh;如果外壳还按
* 布局视口排版,正文和发送按钮就会落到键盘后面。dvh 是 CSS 兜底,这个变量
* 处理 iOS WebView 与部分旧 Chromium 对动态视口更新不及时的情况。
*/
const syncVisibleViewport = () => {
const viewport = window.visualViewport;
const height = viewport?.height ?? window.innerHeight;
document.documentElement.style.setProperty('--app-height', `${Math.round(height)}px`);
};
syncVisibleViewport();
window.addEventListener('resize', syncVisibleViewport, { passive: true });
// 旋屏:iOS 上部分版本的 resize 不跟着方向变化走,显式再同步一次(并延后一帧,
// 让浏览器先把视口尺寸结算完 —— 立刻读会拿到旧高度)
window.addEventListener('orientationchange', () => {
syncVisibleViewport();
requestAnimationFrame(syncVisibleViewport);
}, { passive: true });
window.visualViewport?.addEventListener('resize', syncVisibleViewport, { passive: true });
window.visualViewport?.addEventListener('scroll', syncVisibleViewport, { passive: true });
// 编辑表单时隐藏窄屏底栏,把有限的键盘上方空间留给正文与操作按钮。
// focusout 要延后一帧:从一个输入框切到另一个时 activeElement 会短暂回到 body。
const syncEditingState = () => {
const el = document.activeElement;
const editing = el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement;
document.documentElement.classList.toggle('form-editing', editing);
};
document.addEventListener('focusin', syncEditingState);
document.addEventListener('focusout', () => requestAnimationFrame(syncEditingState));
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
{/*
背景层挂在 App **之外**。
挂在 App 里面的话,它只会存在于主界面的那几个分支上 —— 登录页、加载页、
初始化向导都各自 return 自己的外层 div。背景是全局装饰,用户不该
「一进登录页背景就没了」。它 position:fixed + z-index:-1,与 App 的
布局无关,放这里最稳。
*/}
<div className="app-backdrop" aria-hidden="true" />
<App />
</React.StrictMode>
);