diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index d7c17b4663..8d29b4c662 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -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()
diff --git a/frontend/src/utils/__tests__/device.spec.ts b/frontend/src/utils/__tests__/device.spec.ts
index fad14e754f..cbb4477bcf 100644
--- a/frontend/src/utils/__tests__/device.spec.ts
+++ b/frontend/src/utils/__tests__/device.spec.ts
@@ -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)
+ })
+})
diff --git a/frontend/src/utils/device.ts b/frontend/src/utils/device.ts
index e60b10fd45..cd3930c5ed 100644
--- a/frontend/src/utils/device.ts
+++ b/frontend/src/utils/device.ts
@@ -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 })
+}
diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue
index 635488479b..05eb74365f 100644
--- a/frontend/src/views/admin/GroupsView.vue
+++ b/frontend/src/views/admin/GroupsView.vue
@@ -1101,7 +1101,7 @@
@@ -2615,7 +2615,7 @@
diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue
index bf749bb73b..af2a95c014 100644
--- a/frontend/src/views/admin/SettingsView.vue
+++ b/frontend/src/views/admin/SettingsView.vue
@@ -834,7 +834,7 @@
-
+
-
+
@@ -6664,7 +6664,7 @@
-
+
-
+