Force portrait layout on phones instead of landscape prompt.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 — 手机端竖屏锁定
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user