用户:「为什么窄屏的横屏和竖屏没有自适应」。 ## 根因 断点是**纯宽度**的:`(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 条)。
68 lines
3.2 KiB
TypeScript
68 lines
3.2 KiB
TypeScript
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>
|
||
);
|