Force portrait layout on phones instead of landscape prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-27 14:26:12 +08:00
parent bc0be9ce1a
commit ed96b3ffb0
4 changed files with 82 additions and 65 deletions
+11
View File
@@ -5,6 +5,17 @@
--- ---
## 2026-07-27 — 手机端真·竖屏(禁横屏布局)
### 变更
1. 去掉「请竖屏使用」遮罩:那只是提示,不是锁定。
2. **能锁系统方向时**(常见:安卓安装到主屏幕的 PWA)调用 `orientation.lock`
3. **锁不住时**(多数浏览器页 / iOS):设备横放也不进入横屏 UI,界面仍按竖屏坐标系渲染。
4. Manifest`orientation` 改为 `portrait-primary`
---
## 2026-07-27 — 手机端竖屏锁定 ## 2026-07-27 — 手机端竖屏锁定
### 变更 ### 变更
+2 -2
View File
@@ -5,8 +5,8 @@
"start_url": "/plan", "start_url": "/plan",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",
"display_override": ["standalone", "minimal-ui", "browser"], "display_override": ["standalone", "fullscreen", "minimal-ui"],
"orientation": "portrait", "orientation": "portrait-primary",
"background_color": "#0b0e11", "background_color": "#0b0e11",
"theme_color": "#0b0e11", "theme_color": "#0b0e11",
"lang": "zh-CN", "lang": "zh-CN",
+42 -36
View File
@@ -1,4 +1,8 @@
/** 手机端尽量锁定竖屏;浏览器页用遮罩提示,已安装 PWA 由 manifest 约束。 */ /**
* 手机端竖屏锁定:
* 1) 能锁系统方向时直接 lock(安卓 PWA / 部分全屏环境)
* 2) 锁不住时,横屏仍强制按竖屏布局渲染(不进入横屏 UI)
*/
type OrientationLockType = type OrientationLockType =
| "any" | "any"
@@ -12,64 +16,66 @@ type OrientationLockType =
function isPhoneLike(): boolean { function isPhoneLike(): boolean {
if (typeof window === "undefined") return false; if (typeof window === "undefined") return false;
const coarse = window.matchMedia("(pointer: coarse)").matches;
const narrow = Math.min(window.screen.width, window.screen.height) <= 520;
const ua = navigator.userAgent || ""; const ua = navigator.userAgent || "";
const phoneUa = /Android.+Mobile|iPhone|iPod|Windows Phone/i.test(ua); if (/Android.+Mobile|iPhone|iPod|Windows Phone/i.test(ua)) return true;
return phoneUa || (coarse && narrow); const coarse = window.matchMedia("(pointer: coarse)").matches;
const shortSide = Math.min(window.screen.width, window.screen.height);
return coarse && shortSide <= 520;
} }
async function tryLockPortrait(): Promise<void> { function isLandscape(): boolean {
return window.matchMedia("(orientation: landscape)").matches;
}
async function tryLockPortrait(): Promise<boolean> {
if (!isPhoneLike()) return false;
const orient = screen.orientation as ScreenOrientation & { const orient = screen.orientation as ScreenOrientation & {
lock?: (orientation: OrientationLockType) => Promise<void>; lock?: (orientation: OrientationLockType) => Promise<void>;
}; };
if (!orient?.lock) return; if (!orient?.lock) return false;
try { try {
await orient.lock("portrait"); await orient.lock("portrait");
return true;
} catch { } catch {
try { try {
await orient.lock("portrait-primary"); await orient.lock("portrait-primary");
return true;
} catch { } catch {
/* 多数浏览器仅全屏/PWA 允许 lock,失败则靠遮罩 */ return false;
} }
} }
} }
function ensureGate(): HTMLElement { function syncForcedPortrait(): void {
let el = document.getElementById("portrait-lock-gate"); const html = document.documentElement;
if (el) return el;
el = document.createElement("div");
el.id = "portrait-lock-gate";
el.className = "portrait-lock-gate";
el.setAttribute("aria-live", "polite");
el.innerHTML =
'<div class="portrait-lock-card">' +
'<p class="portrait-lock-title">请竖屏使用</p>' +
'<p class="portrait-lock-sub">手机端仅支持竖屏浏览</p>' +
"</div>";
document.body.appendChild(el);
return el;
}
function syncGate(): void {
if (!isPhoneLike()) { if (!isPhoneLike()) {
const el = document.getElementById("portrait-lock-gate"); html.classList.remove("phone-portrait-lock", "phone-landscape");
if (el) el.classList.remove("show");
return; return;
} }
const landscape = window.matchMedia("(orientation: landscape)").matches; html.classList.add("phone-portrait-lock");
ensureGate().classList.toggle("show", landscape); html.classList.toggle("phone-landscape", isLandscape());
if (!landscape) void tryLockPortrait();
} }
/** 注册竖屏锁定与横屏提示遮罩。 */ /** 手机端:系统竖屏锁 + 横屏时强制竖屏排版。 */
export function initPortraitLock(): void { export function initPortraitLock(): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
syncGate();
window.addEventListener("orientationchange", syncGate); const sync = () => {
window.addEventListener("resize", syncGate); syncForcedPortrait();
void tryLockPortrait();
};
sync();
window.addEventListener("orientationchange", sync);
window.addEventListener("resize", sync);
document.addEventListener("visibilitychange", () => { document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") syncGate(); if (document.visibilityState === "visible") sync();
}); });
void tryLockPortrait();
// 部分浏览器要求用户手势后才能 orientation.lock
const onGesture = () => {
void tryLockPortrait().then(() => syncForcedPortrait());
};
window.addEventListener("pointerdown", onGesture, { passive: true });
window.addEventListener("touchstart", onGesture, { passive: true });
} }
+27 -27
View File
@@ -33,37 +33,37 @@ body {
var(--bg); var(--bg);
} }
.portrait-lock-gate { /*
display: none; * 手机横屏时不进入横屏布局:把 #root 旋成竖屏坐标系。
* 能调用 screen.orientation.lock 的环境(如安卓已安装 PWA)则系统本身不转,此类样式不生效。
*/
html.phone-portrait-lock.phone-landscape,
html.phone-portrait-lock.phone-landscape body {
width: 100%;
height: 100%;
overflow: hidden;
overscroll-behavior: none;
}
html.phone-portrait-lock.phone-landscape body {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(11, 14, 17, 0.96);
text-align: center;
} }
.portrait-lock-gate.show { html.phone-portrait-lock.phone-landscape #root {
display: flex; position: absolute;
} width: 100vh;
width: 100dvh;
.portrait-lock-card { height: 100vw;
max-width: 280px; height: 100dvw;
} top: 100%;
top: 100dvh;
.portrait-lock-title { left: 0;
margin: 0 0 8px; transform: rotate(-90deg);
font-size: 20px; transform-origin: left top;
font-weight: 700; overflow: auto;
color: var(--accent); -webkit-overflow-scrolling: touch;
} overscroll-behavior: none;
.portrait-lock-sub {
margin: 0;
font-size: 14px;
color: var(--muted);
} }
button, button,