Lock phone UI to portrait with PWA and landscape gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-27 — 手机端竖屏锁定
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
1. **PWA**:`manifest.webmanifest` 的 `orientation` 改为 `portrait`(安装到主屏幕后系统侧竖屏)。
|
||||||
|
2. **浏览器**:手机横屏时全屏提示「请竖屏使用」;支持时尝试 `screen.orientation.lock`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-27 — 手机端期权卡 / 交易记录排版
|
## 2026-07-27 — 手机端期权卡 / 交易记录排版
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scope": "/",
|
"scope": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"display_override": ["standalone", "minimal-ui", "browser"],
|
"display_override": ["standalone", "minimal-ui", "browser"],
|
||||||
"orientation": "any",
|
"orientation": "portrait",
|
||||||
"background_color": "#0b0e11",
|
"background_color": "#0b0e11",
|
||||||
"theme_color": "#0b0e11",
|
"theme_color": "#0b0e11",
|
||||||
"lang": "zh-CN",
|
"lang": "zh-CN",
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { registerServiceWorker } from "./pwa/install";
|
import { registerServiceWorker } from "./pwa/install";
|
||||||
|
import { initPortraitLock } from "./pwa/portraitLock";
|
||||||
import "./styles/app.css";
|
import "./styles/app.css";
|
||||||
|
|
||||||
registerServiceWorker();
|
registerServiceWorker();
|
||||||
|
initPortraitLock();
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/** 手机端尽量锁定竖屏;浏览器页用遮罩提示,已安装 PWA 由 manifest 约束。 */
|
||||||
|
|
||||||
|
type OrientationLockType =
|
||||||
|
| "any"
|
||||||
|
| "natural"
|
||||||
|
| "landscape"
|
||||||
|
| "portrait"
|
||||||
|
| "portrait-primary"
|
||||||
|
| "portrait-secondary"
|
||||||
|
| "landscape-primary"
|
||||||
|
| "landscape-secondary";
|
||||||
|
|
||||||
|
function isPhoneLike(): boolean {
|
||||||
|
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 phoneUa = /Android.+Mobile|iPhone|iPod|Windows Phone/i.test(ua);
|
||||||
|
return phoneUa || (coarse && narrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryLockPortrait(): Promise<void> {
|
||||||
|
const orient = screen.orientation as ScreenOrientation & {
|
||||||
|
lock?: (orientation: OrientationLockType) => Promise<void>;
|
||||||
|
};
|
||||||
|
if (!orient?.lock) return;
|
||||||
|
try {
|
||||||
|
await orient.lock("portrait");
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
await orient.lock("portrait-primary");
|
||||||
|
} catch {
|
||||||
|
/* 多数浏览器仅全屏/PWA 允许 lock,失败则靠遮罩 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureGate(): HTMLElement {
|
||||||
|
let el = document.getElementById("portrait-lock-gate");
|
||||||
|
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()) {
|
||||||
|
const el = document.getElementById("portrait-lock-gate");
|
||||||
|
if (el) el.classList.remove("show");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const landscape = window.matchMedia("(orientation: landscape)").matches;
|
||||||
|
ensureGate().classList.toggle("show", landscape);
|
||||||
|
if (!landscape) void tryLockPortrait();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注册竖屏锁定与横屏提示遮罩。 */
|
||||||
|
export function initPortraitLock(): void {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
syncGate();
|
||||||
|
window.addEventListener("orientationchange", syncGate);
|
||||||
|
window.addEventListener("resize", syncGate);
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "visible") syncGate();
|
||||||
|
});
|
||||||
|
void tryLockPortrait();
|
||||||
|
}
|
||||||
@@ -33,6 +33,39 @@ body {
|
|||||||
var(--bg);
|
var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.portrait-lock-gate {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
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 {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait-lock-card {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait-lock-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portrait-lock-sub {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input {
|
input {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|||||||
Reference in New Issue
Block a user