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:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user