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 条)。
This commit is contained in:
2026-09-14 11:13:24 +08:00
parent 0a4b98144c
commit 8a9bc58433
4 changed files with 110 additions and 1 deletions

View File

@ -28,7 +28,7 @@ import AdminUsersPage from './components/AdminUsersPage';
const CalendarView = lazy(() => import('./components/CalendarView'));
import NarrowNav from './components/NarrowNav';
import NarrowStack from './components/NarrowStack';
import { useIsNarrow } from './hooks/useIsNarrow';
import { useCompactTwoPane, useIsNarrow } from './hooks/useIsNarrow';
export default function App() {
const phase = useAuthStore(s => s.phase);
@ -40,6 +40,8 @@ export default function App() {
const resetUI = useUIStore(s => s.reset);
const narrowPane = useUIStore(s => s.narrowPane);
const narrow = useIsNarrow();
// 宽而矮(手机横屏)时紧凑外壳内容区改并排,见 useCompactTwoPane 的说明
const compactTwoPane = useCompactTwoPane();
const commTab = useUIStore(s => s.commTab);
/*
@ -219,6 +221,15 @@ export default function App() {
</NarrowShell>
);
}
// 宽而矮:列表与详情并排(横屏自适应)。底部导航与悬浮加号保持不变 ——
// 它们已经是紧凑外壳的一部分,换的只是内容区的排布方式。
if (compactTwoPane && hasList) {
return (
<NarrowShell>
<div className="flex-1 min-h-0 flex">{list}{main}</div>
</NarrowShell>
);
}
return (
<NarrowShell>
<NarrowStack base={list} overlay={main} open={narrowPane === 'detail'} />

View File

@ -28,3 +28,32 @@ export function useIsNarrow(): boolean {
return narrow;
}
/**
* 紧凑**双栏**判定:宽而矮的屏幕(手机横屏 844×390、小平板 1024×768 之类)。
*
* 用户2026-09-14「为什么窄屏的横屏和竖屏没有自适应」。
*
* 原因是纯宽度断点:竖屏 390 与横屏 844 **都 < 1024** ⇒ 一直是"窄屏" ⇒
* 布局一个像素都不变,横屏多出来的 450px 全浪费在空白上。
* 但横屏其实放得下「列表 + 详情」并排320 + 至少 420所以这里单独判定
* 仍是紧凑外壳(底部导航),但内容区从"覆盖式"改成"并排式"。
*
* 用高度而不是方向词landscape桌面窗口压扁、分屏、开软键盘都会命中
* 而它们要的正是同一件事 —— 别浪费横向空间。
*/
export const COMPACT_TWO_PANE_QUERY = '(min-width: 700px) and (max-height: 560px)';
export function useCompactTwoPane(): boolean {
const [on, setOn] = useState(() =>
typeof window === 'undefined' ? false : window.matchMedia(COMPACT_TWO_PANE_QUERY).matches
);
useEffect(() => {
const mq = window.matchMedia(COMPACT_TWO_PANE_QUERY);
const onChange = (e: MediaQueryListEvent) => setOn(e.matches);
mq.addEventListener('change', onChange);
setOn(mq.matches);
return () => mq.removeEventListener('change', onChange);
}, []);
return on;
}

View File

@ -32,6 +32,12 @@ const syncVisibleViewport = () => {
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 });