fix(ios): 修复 iOS 端输入框聚焦导致页面自动缩放

iOS Safari 在输入框字号小于 16px 时聚焦会自动放大页面,且失焦后不会恢复,
导致布局错位。启动时仅对 iOS 设备为 viewport 注入 maximum-scale=1 阻止该
行为;iOS 10+ 用户仍可双指手动缩放,Android 不受影响。

- utils/device.ts 新增 detectIOSDevice/isIOSDevice(含 iPadOS 桌面模式识别)
- main.ts 启动阶段执行 initIOSViewportZoomFix
- 补充 detectIOSDevice 单元测试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
J606y
2026-07-20 21:58:46 +08:00
co-authored by Claude Fable 5
parent e625ce3b3b
commit 9e072cefec
3 changed files with 76 additions and 1 deletions
+16
View File
@@ -5,8 +5,23 @@ import router from './router'
import i18n, { initI18n } from './i18n'
import { useAppStore } from '@/stores/app'
import { updateFavicon } from '@/utils/branding'
import { isIOSDevice } from '@/utils/device'
import './style.css'
function initIOSViewportZoomFix() {
// iOS Safari 在输入框字号小于 16px 时聚焦会自动放大页面,且失焦后不会恢复。
// 限制 maximum-scale 可阻止该行为;iOS 10+ 用户仍可双指手动缩放,不影响可访问性。
// 仅在 iOS 设备上注入,避免影响 Android Chrome 的手动缩放能力。
if (!isIOSDevice()) return
const viewport = document.querySelector('meta[name="viewport"]')
if (!viewport) return
const content = viewport.getAttribute('content') || ''
if (/maximum-scale/i.test(content)) return
viewport.setAttribute('content', `${content}, maximum-scale=1.0`)
}
function initThemeClass() {
const savedTheme = localStorage.getItem('theme')
const shouldUseDark =
@@ -18,6 +33,7 @@ function initThemeClass() {
async function bootstrap() {
// Apply theme class globally before app mount to keep all routes consistent.
initThemeClass()
initIOSViewportZoomFix()
const app = createApp(App)
const pinia = createPinia()
+41 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { detectMobileDevice } from '../device'
import { detectIOSDevice, detectMobileDevice } from '../device'
describe('detectMobileDevice', () => {
it('prefers userAgentData.mobile when available', () => {
@@ -53,3 +53,43 @@ describe('detectMobileDevice', () => {
})).toBe(false)
})
})
describe('detectIOSDevice', () => {
it('recognizes iPhone from the UA token', () => {
expect(detectIOSDevice({
navigator: {
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 Version/17.5 Mobile/15E148 Safari/604.1',
maxTouchPoints: 5,
},
})).toBe(true)
})
it('recognizes iPadOS desktop mode via touch capability', () => {
expect(detectIOSDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 Version/17.0 Safari/605.1.15',
platform: 'MacIntel',
maxTouchPoints: 5,
},
})).toBe(true)
})
it('keeps Android devices as non-iOS', () => {
expect(detectIOSDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/136.0 Mobile Safari/537.36',
maxTouchPoints: 5,
},
})).toBe(false)
})
it('keeps desktop macOS without touch as non-iOS', () => {
expect(detectIOSDevice({
navigator: {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/136.0 Safari/537.36',
platform: 'MacIntel',
maxTouchPoints: 0,
},
})).toBe(false)
})
})
+19
View File
@@ -20,6 +20,7 @@ interface DeviceDetectionEnvironment {
const MOBILE_UA_RE = /\b(Mobi|Android|iPhone|iPod|Windows Phone|webOS|BlackBerry|IEMobile)\b/i
const TABLET_UA_RE = /\b(iPad|Tablet)\b/i
const IOS_UA_RE = /\b(iPhone|iPad|iPod)\b/i
function matchesQuery(
matchMedia: DeviceDetectionEnvironment['matchMedia'],
@@ -60,3 +61,21 @@ export function isMobileDevice(): boolean {
matchMedia: typeof window !== 'undefined' ? window.matchMedia.bind(window) : undefined,
})
}
export function detectIOSDevice(env: DeviceDetectionEnvironment = {}): boolean {
const nav = env.navigator
if (!nav) return false
const userAgent = nav.userAgent || ''
const maxTouchPoints = nav.maxTouchPoints ?? 0
// iPadOS 13+ 桌面模式下 UA 伪装成 macOS,需要靠触控点数识别。
const isIPadOSDesktopMode = nav.platform === 'MacIntel' && maxTouchPoints > 1
return IOS_UA_RE.test(userAgent) || isIPadOSDesktopMode
}
export function isIOSDevice(): boolean {
if (typeof navigator === 'undefined') return false
return detectIOSDevice({ navigator })
}